Layout

Sidebar.

A composable app sidebar that collapses off canvas or down to icons, and turns into a sheet on phones.

North hub/Overview
Turtles online
3 / 4
Fuel stored
12,480
Guestbook
12 new
Recent activity
  • > miner-02 returned to base with 64 coal
  • > new guestbook entry from computer 17
  • > relay handshake ok on lantern-main

Installation#

With the CLI

$npx lanterncn add sidebar

This also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/sidebar. See Installation if your project is not set up yet.

Manually

  1. Install the dependencies

    npm install radix-ui lucide-react class-variance-authority
  2. Copy the source into your project

    components/ui/sidebar.tsx
    "use client";
    
    import * as React from "react";
    import { cva, type VariantProps } from "class-variance-authority";
    import { Slot } from "radix-ui";
    import { PanelLeftIcon } from "lucide-react";
    
    import { cn } from "@/lib/utils";
    import { useIsMobile } from "@/hooks/use-mobile";
    import { Button } from "@/components/ui/button";
    import { Input } from "@/components/ui/input";
    import { Separator } from "@/components/ui/separator";
    import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet";
    import { Skeleton } from "@/components/ui/skeleton";
    import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
    
    const SIDEBAR_WIDTH = "16rem";
    const SIDEBAR_WIDTH_MOBILE = "18rem";
    const SIDEBAR_WIDTH_ICON = "3rem";
    const SIDEBAR_KEYBOARD_SHORTCUT = "b";
    
    type SidebarContextProps = {
      state: "expanded" | "collapsed";
      open: boolean;
      setOpen: (open: boolean | ((open: boolean) => boolean)) => void;
      openMobile: boolean;
      setOpenMobile: (open: boolean | ((open: boolean) => boolean)) => void;
      isMobile: boolean;
      toggleSidebar: () => void;
    };
    
    const SidebarContext = React.createContext<SidebarContextProps | null>(null);
    
    function useSidebar() {
      const context = React.useContext(SidebarContext);
      if (!context) {
        throw new Error("useSidebar must be used within a SidebarProvider.");
      }
      return context;
    }
    
    function SidebarProvider({
      defaultOpen = true,
      open: openProp,
      onOpenChange: setOpenProp,
      keyboardShortcut = true,
      className,
      style,
      children,
      ...props
    }: React.ComponentProps<"div"> & {
      defaultOpen?: boolean;
      open?: boolean;
      onOpenChange?: (open: boolean) => void;
      /** Toggle with Cmd/Ctrl+B. Turn off when several sidebars share a page. */
      keyboardShortcut?: boolean;
    }) {
      const isMobile = useIsMobile();
      const [openMobile, setOpenMobile] = React.useState(false);
    
      // Internal state unless the caller controls `open`. Nothing is written to cookies or storage.
      const [_open, _setOpen] = React.useState(defaultOpen);
      const open = openProp ?? _open;
      const setOpen = React.useCallback(
        (value: boolean | ((value: boolean) => boolean)) => {
          const openState = typeof value === "function" ? value(open) : value;
          if (setOpenProp) setOpenProp(openState);
          else _setOpen(openState);
        },
        [setOpenProp, open],
      );
    
      const toggleSidebar = React.useCallback(() => {
        if (isMobile) setOpenMobile((o) => !o);
        else setOpen((o) => !o);
      }, [isMobile, setOpen]);
    
      React.useEffect(() => {
        if (!keyboardShortcut) return;
        const handleKeyDown = (event: KeyboardEvent) => {
          if (event.key.toLowerCase() === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey) && !event.altKey) {
            event.preventDefault();
            toggleSidebar();
          }
        };
        window.addEventListener("keydown", handleKeyDown);
        return () => window.removeEventListener("keydown", handleKeyDown);
      }, [keyboardShortcut, toggleSidebar]);
    
      const state = open ? "expanded" : "collapsed";
    
      const contextValue = React.useMemo<SidebarContextProps>(
        () => ({ state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar }),
        [state, open, setOpen, isMobile, openMobile, toggleSidebar],
      );
    
      return (
        <SidebarContext.Provider value={contextValue}>
          <TooltipProvider delayDuration={0}>
            <div
              data-slot="sidebar-wrapper"
              style={
                {
                  "--sidebar-width": SIDEBAR_WIDTH,
                  "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
                  ...style,
                } as React.CSSProperties
              }
              className={cn("group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar", className)}
              {...props}
            >
              {children}
            </div>
          </TooltipProvider>
        </SidebarContext.Provider>
      );
    }
    
    function Sidebar({
      side = "left",
      variant = "sidebar",
      collapsible = "offcanvas",
      className,
      children,
      ...props
    }: React.ComponentProps<"div"> & {
      side?: "left" | "right";
      variant?: "sidebar" | "floating" | "inset";
      collapsible?: "offcanvas" | "icon" | "none";
    }) {
      const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
    
      if (collapsible === "none") {
        return (
          <div
            data-slot="sidebar"
            className={cn("flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground", className)}
            {...props}
          >
            {children}
          </div>
        );
      }
    
      if (isMobile) {
        return (
          <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
            <SheetContent
              data-sidebar="sidebar"
              data-slot="sidebar"
              data-mobile="true"
              side={side}
              showCloseButton={false}
              className="w-(--sidebar-width) max-w-[85vw] gap-0 overflow-hidden bg-sidebar p-0 text-sidebar-foreground sm:max-w-[85vw]"
              style={{ "--sidebar-width": SIDEBAR_WIDTH_MOBILE } as React.CSSProperties}
            >
              <SheetHeader className="sr-only">
                <SheetTitle>Sidebar</SheetTitle>
                <SheetDescription>Displays the mobile sidebar.</SheetDescription>
              </SheetHeader>
              <div className="flex h-full w-full flex-col">{children}</div>
            </SheetContent>
          </Sheet>
        );
      }
    
      return (
        <div
          className="group peer hidden text-sidebar-foreground md:block"
          data-state={state}
          data-collapsible={state === "collapsed" ? collapsible : ""}
          data-variant={variant}
          data-side={side}
          data-slot="sidebar"
        >
          {/* Takes up the sidebar's width in the layout so the inset sits beside it. */}
          <div
            data-slot="sidebar-gap"
            className={cn(
              "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
              "group-data-[collapsible=offcanvas]:w-0",
              "group-data-[side=right]:rotate-180",
              variant === "floating" || variant === "inset"
                ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
                : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
            )}
          />
          <div
            data-slot="sidebar-container"
            className={cn(
              "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
              side === "left"
                ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
                : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
              variant === "floating" || variant === "inset"
                ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
                : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
              "border-sidebar-border",
              className,
            )}
            {...props}
          >
            <div
              data-sidebar="sidebar"
              data-slot="sidebar-inner"
              className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-block-sm"
            >
              {children}
            </div>
          </div>
        </div>
      );
    }
    
    function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>) {
      const { toggleSidebar, open, openMobile, isMobile } = useSidebar();
    
      return (
        <Button
          data-sidebar="trigger"
          data-slot="sidebar-trigger"
          variant="ghost"
          size="icon-sm"
          aria-expanded={isMobile ? openMobile : open}
          className={cn("size-8", className)}
          onClick={(event) => {
            onClick?.(event);
            toggleSidebar();
          }}
          {...props}
        >
          <PanelLeftIcon />
          <span className="sr-only">Toggle sidebar</span>
        </Button>
      );
    }
    
    function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
      const { toggleSidebar } = useSidebar();
    
      return (
        <button
          data-sidebar="rail"
          data-slot="sidebar-rail"
          aria-label="Toggle sidebar"
          tabIndex={-1}
          onClick={toggleSidebar}
          title="Toggle sidebar"
          className={cn(
            "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-px after:transition-colors hover:after:bg-sidebar-primary sm:flex",
            "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
            "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
            "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar",
            "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
            "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
            className,
          )}
          {...props}
        />
      );
    }
    
    function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
      return (
        <main
          data-slot="sidebar-inset"
          className={cn(
            "relative flex w-full min-w-0 flex-1 flex-col bg-background",
            "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-lg md:peer-data-[variant=inset]:border md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
            className,
          )}
          {...props}
        />
      );
    }
    
    function SidebarInput({ className, ...props }: React.ComponentProps<typeof Input>) {
      return (
        <Input
          data-slot="sidebar-input"
          data-sidebar="input"
          className={cn("h-8 w-full bg-background text-[13px]", className)}
          {...props}
        />
      );
    }
    
    function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
      return <div data-slot="sidebar-header" data-sidebar="header" className={cn("flex flex-col gap-2 p-2", className)} {...props} />;
    }
    
    function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
      return <div data-slot="sidebar-footer" data-sidebar="footer" className={cn("flex flex-col gap-2 p-2", className)} {...props} />;
    }
    
    function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
      return (
        <Separator
          data-slot="sidebar-separator"
          data-sidebar="separator"
          className={cn("mx-2 w-auto bg-sidebar-border data-[orientation=horizontal]:w-auto", className)}
          {...props}
        />
      );
    }
    
    function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
      return (
        <div
          data-slot="sidebar-content"
          data-sidebar="content"
          className={cn(
            "flex min-h-0 flex-1 flex-col gap-2 overflow-auto [scrollbar-color:var(--color-sidebar-border)_transparent] [scrollbar-width:thin] group-data-[collapsible=icon]:overflow-hidden",
            className,
          )}
          {...props}
        />
      );
    }
    
    function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
      return (
        <div
          data-slot="sidebar-group"
          data-sidebar="group"
          className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
          {...props}
        />
      );
    }
    
    function SidebarGroupLabel({
      className,
      asChild = false,
      ...props
    }: React.ComponentProps<"div"> & { asChild?: boolean }) {
      const Comp = asChild ? Slot.Root : "div";
    
      return (
        <Comp
          data-slot="sidebar-group-label"
          data-sidebar="group-label"
          className={cn(
            "flex h-8 shrink-0 items-center rounded-md px-2 font-mono text-[10px] font-semibold tracking-[0.2em] text-success uppercase outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 focus-visible:ring-sidebar-ring/40 [&>svg]:size-4 [&>svg]:shrink-0",
            "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
            className,
          )}
          {...props}
        />
      );
    }
    
    function SidebarGroupAction({
      className,
      asChild = false,
      ...props
    }: React.ComponentProps<"button"> & { asChild?: boolean }) {
      const Comp = asChild ? Slot.Root : "button";
    
      return (
        <Comp
          data-slot="sidebar-group-action"
          data-sidebar="group-action"
          className={cn(
            "absolute top-3.5 right-3 flex aspect-square w-5 cursor-pointer items-center justify-center rounded-sm p-0 text-muted-foreground outline-hidden transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring/40 [&>svg]:size-4 [&>svg]:shrink-0",
            // Bigger hit area on mobile.
            "after:absolute after:-inset-2 md:after:hidden",
            "group-data-[collapsible=icon]:hidden",
            className,
          )}
          {...props}
        />
      );
    }
    
    function SidebarGroupContent({ className, ...props }: React.ComponentProps<"div">) {
      return (
        <div data-slot="sidebar-group-content" data-sidebar="group-content" className={cn("w-full text-sm", className)} {...props} />
      );
    }
    
    function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
      return (
        <ul data-slot="sidebar-menu" data-sidebar="menu" className={cn("flex w-full min-w-0 flex-col gap-px", className)} {...props} />
      );
    }
    
    function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
      return (
        <li data-slot="sidebar-menu-item" data-sidebar="menu-item" className={cn("group/menu-item relative", className)} {...props} />
      );
    }
    
    const sidebarMenuButtonVariants = cva(
      [
        "peer/menu-button flex w-full cursor-pointer items-center gap-2 overflow-hidden rounded-r-md border-l border-transparent p-2 text-left text-[13.5px] text-muted-foreground outline-hidden transition-[width,height,padding,color,background-color,border-color]",
        "hover:border-input hover:bg-sidebar-accent/60 hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring/40 active:bg-sidebar-accent",
        "disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50",
        "group-has-data-[sidebar=menu-action]/menu-item:pr-8",
        "data-[active=true]:border-sidebar-primary data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[active=true]:[&>svg]:text-sidebar-primary",
        "data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground",
        "group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2!",
        "[&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
      ].join(" "),
      {
        variants: {
          variant: {
            default: "",
            outline:
              "rounded-md border border-sidebar-border bg-background hover:border-input data-[active=true]:border-sidebar-primary",
          },
          size: {
            default: "h-8",
            sm: "h-7 text-xs",
            lg: "h-12 group-data-[collapsible=icon]:p-0!",
          },
        },
        defaultVariants: { variant: "default", size: "default" },
      },
    );
    
    function SidebarMenuButton({
      asChild = false,
      isActive = false,
      variant = "default",
      size = "default",
      tooltip,
      className,
      ...props
    }: React.ComponentProps<"button"> & {
      asChild?: boolean;
      isActive?: boolean;
      tooltip?: string | React.ComponentProps<typeof TooltipContent>;
    } & VariantProps<typeof sidebarMenuButtonVariants>) {
      const Comp = asChild ? Slot.Root : "button";
      const { isMobile, state } = useSidebar();
    
      const button = (
        <Comp
          data-slot="sidebar-menu-button"
          data-sidebar="menu-button"
          data-size={size}
          data-active={isActive}
          aria-current={isActive ? "page" : undefined}
          className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
          {...props}
        />
      );
    
      if (!tooltip) return button;
    
      const tooltipProps = typeof tooltip === "string" ? { children: tooltip } : tooltip;
    
      return (
        <Tooltip>
          <TooltipTrigger asChild>{button}</TooltipTrigger>
          <TooltipContent side="right" align="center" hidden={state !== "collapsed" || isMobile} {...tooltipProps} />
        </Tooltip>
      );
    }
    
    function SidebarMenuAction({
      className,
      asChild = false,
      showOnHover = false,
      ...props
    }: React.ComponentProps<"button"> & { asChild?: boolean; showOnHover?: boolean }) {
      const Comp = asChild ? Slot.Root : "button";
    
      return (
        <Comp
          data-slot="sidebar-menu-action"
          data-sidebar="menu-action"
          className={cn(
            "absolute top-1.5 right-1 flex aspect-square w-5 cursor-pointer items-center justify-center rounded-sm p-0 text-muted-foreground outline-hidden transition-colors hover:bg-sidebar-border hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring/40 peer-hover/menu-button:text-sidebar-foreground [&>svg]:size-4 [&>svg]:shrink-0",
            "after:absolute after:-inset-2 md:after:hidden",
            "peer-data-[size=sm]/menu-button:top-1 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-3.5",
            "group-data-[collapsible=icon]:hidden",
            showOnHover &&
              "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-foreground data-[state=open]:opacity-100 md:opacity-0",
            className,
          )}
          {...props}
        />
      );
    }
    
    function SidebarMenuBadge({ className, ...props }: React.ComponentProps<"div">) {
      return (
        <div
          data-slot="sidebar-menu-badge"
          data-sidebar="menu-badge"
          className={cn(
            "pointer-events-none absolute right-1.5 flex h-5 min-w-5 items-center justify-center rounded-sm px-1 font-mono text-[10px] tabular-nums tracking-wider text-muted-foreground select-none",
            "peer-hover/menu-button:text-sidebar-foreground peer-data-[active=true]/menu-button:text-sidebar-primary",
            "peer-data-[size=sm]/menu-button:top-1 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-3.5",
            "group-data-[collapsible=icon]:hidden",
            className,
          )}
          {...props}
        />
      );
    }
    
    function SidebarMenuSkeleton({
      className,
      showIcon = false,
      ...props
    }: React.ComponentProps<"div"> & { showIcon?: boolean }) {
      // A stable width between 50% and 90%, derived from the id so server and client agree.
      const id = React.useId();
      const width = React.useMemo(() => {
        let hash = 0;
        for (let i = 0; i < id.length; i++) hash = (hash * 31 + id.charCodeAt(i)) >>> 0;
        return `${50 + (hash % 41)}%`;
      }, [id]);
    
      return (
        <div
          data-slot="sidebar-menu-skeleton"
          data-sidebar="menu-skeleton"
          className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
          {...props}
        >
          {showIcon && <Skeleton className="size-4 rounded-sm" data-sidebar="menu-skeleton-icon" />}
          <Skeleton
            className="h-3.5 max-w-(--skeleton-width) flex-1"
            data-sidebar="menu-skeleton-text"
            style={{ "--skeleton-width": width } as React.CSSProperties}
          />
        </div>
      );
    }
    
    function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
      return (
        <ul
          data-slot="sidebar-menu-sub"
          data-sidebar="menu-sub"
          className={cn(
            "mx-3.5 flex min-w-0 translate-x-px flex-col gap-px border-l border-sidebar-border px-2.5 py-0.5",
            "group-data-[collapsible=icon]:hidden",
            className,
          )}
          {...props}
        />
      );
    }
    
    function SidebarMenuSubItem({ className, ...props }: React.ComponentProps<"li">) {
      return (
        <li
          data-slot="sidebar-menu-sub-item"
          data-sidebar="menu-sub-item"
          className={cn("group/menu-sub-item relative", className)}
          {...props}
        />
      );
    }
    
    function SidebarMenuSubButton({
      asChild = false,
      size = "md",
      isActive = false,
      className,
      ...props
    }: React.ComponentProps<"a"> & { asChild?: boolean; size?: "sm" | "md"; isActive?: boolean }) {
      const Comp = asChild ? Slot.Root : "a";
    
      return (
        <Comp
          data-slot="sidebar-menu-sub-button"
          data-sidebar="menu-sub-button"
          data-size={size}
          data-active={isActive}
          aria-current={isActive ? "page" : undefined}
          className={cn(
            "flex h-7 min-w-0 -translate-x-px cursor-pointer items-center gap-2 overflow-hidden rounded-md px-2 text-muted-foreground outline-hidden transition-colors hover:bg-sidebar-accent/60 hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring/40 active:bg-sidebar-accent",
            "disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50",
            "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-[active=true]:[&>svg]:text-sidebar-primary",
            "[&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
            size === "sm" && "text-xs",
            size === "md" && "text-[13px]",
            "group-data-[collapsible=icon]:hidden",
            className,
          )}
          {...props}
        />
      );
    }
    
    export {
      Sidebar,
      SidebarContent,
      SidebarFooter,
      SidebarGroup,
      SidebarGroupAction,
      SidebarGroupContent,
      SidebarGroupLabel,
      SidebarHeader,
      SidebarInput,
      SidebarInset,
      SidebarMenu,
      SidebarMenuAction,
      SidebarMenuBadge,
      SidebarMenuButton,
      SidebarMenuItem,
      SidebarMenuSkeleton,
      SidebarMenuSub,
      SidebarMenuSubButton,
      SidebarMenuSubItem,
      SidebarProvider,
      SidebarRail,
      SidebarSeparator,
      SidebarTrigger,
      useSidebar,
    };
    hooks/use-mobile.ts
    import * as React from "react";
    
    const MOBILE_BREAKPOINT = 768;
    const QUERY = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`;
    
    function subscribe(onChange: () => void) {
      const mql = window.matchMedia(QUERY);
      mql.addEventListener("change", onChange);
      return () => mql.removeEventListener("change", onChange);
    }
    
    /** True below the md breakpoint (768px). Always false during server rendering. */
    export function useIsMobile() {
      return React.useSyncExternalStore(
        subscribe,
        () => window.matchMedia(QUERY).matches,
        () => false,
      );
    }
  3. Update the import paths to match your project

    The source imports cn from @/lib/utils and uses button, input, separator, sheet, skeleton, tooltip.

Usage#

import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupAction,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInput,
  SidebarInset,
  SidebarMenu,
  SidebarMenuAction,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSkeleton,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarSeparator,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar";
<SidebarProvider>
  <Sidebar collapsible="icon">
    <SidebarContent>
      <SidebarGroup>
        <SidebarGroupLabel>Hub</SidebarGroupLabel>
        <SidebarMenu>
          <SidebarMenuItem>
            <SidebarMenuButton isActive tooltip="Servers">
              <ServerIcon />
              <span>Servers</span>
            </SidebarMenuButton>
          </SidebarMenuItem>
        </SidebarMenu>
      </SidebarGroup>
    </SidebarContent>
  </Sidebar>
  <SidebarInset>
    <SidebarTrigger />
  </SidebarInset>
</SidebarProvider>

SidebarProvider holds the open state in React state only; nothing is written to cookies. Pass open and onOpenChange to control it yourself. Cmd+B or Ctrl+B toggles it; set keyboardShortcut={false} when a page has more than one sidebar.

Set variant to sidebar, floating or inset, and collapsible to offcanvas, icon or none. Below 768px the sidebar opens as a sheet from SidebarTrigger.

The sidebar is fixed to the viewport by default. To embed it in a box, give SidebarProvider className="relative h-full min-h-0" and Sidebar className="absolute h-full", as the examples here do.

The use-mobile hook is installed alongside the component at hooks/use-mobile.ts.

Examples#

Collapse to icons

Servers

Collapsed to icons. Hover an icon to see its name, or press the toggle to expand.

Computer 17Online
Computer 42Busy
Computer 58Online

Floating

Guestbook
Thanks for the relay tips. - miner-02
Hub looks great from spawn.
Left some coal in the chest.