zzzUI

Sidebar

The column down the side of an app: a black, hatched strip whose rows are the Dropdown menu's, the current page filled with the accent.

Install

terminal
# once per project: where @zzzui items come frompnpm dlx shadcn@latest registry add @zzzui=https://zzzui.catforest.dev/r/{name}.jsonpnpm dlx shadcn@latest add @zzzui/sidebar

Examples

An app's sidebar, folding to icons

Board

Press the button or ⌘B / Ctrl+B to fold the sidebar to its icons.

sidebar-app.tsx
import {  ArrowRightIcon,  ExternalIcon,  InfoIcon,  LockIcon,  SearchIcon,} from "@/components/zero/icons";import {  Sidebar,  SidebarContent,  SidebarFooter,  SidebarGroup,  SidebarGroupLabel,  SidebarHeader,  SidebarInset,  SidebarMenu,  SidebarMenuBadge,  SidebarMenuButton,  SidebarMenuItem,  SidebarMenuSub,  SidebarMenuSubButton,  SidebarMenuSubItem,  SidebarProvider,  SidebarSeparator,  SidebarTrigger,} from "@/components/zero/sidebar";const jobs = [  { label: "Board", icon: SearchIcon, count: 12, active: true },  { label: "Taken", icon: ArrowRightIcon, count: 3 },  { label: "Locked", icon: LockIcon },];export default function SidebarApp() {  return (    // A box instead of a whole page: the sidebar's container is absolute here, not fixed.    <SidebarProvider className="relative h-[26rem] min-h-0 w-full overflow-hidden border-(length:--zero-stroke-thin) border-zero-line">      <Sidebar collapsible="icon" className="absolute h-full">        <SidebarHeader>          <div className="flex items-center gap-3 px-3 py-2">            <span className="h-5 w-3 shrink-0 -skew-x-(--zero-slant) bg-zero-accent" />            <span className="truncate font-black font-zero text-zero-lg italic group-data-[collapsible=icon]/sidebar:hidden">              Dispatch            </span>          </div>        </SidebarHeader>        <SidebarContent>          <SidebarGroup>            <SidebarGroupLabel>Jobs</SidebarGroupLabel>            <SidebarMenu>              {jobs.map(({ label, icon: Icon, count, active }) => (                <SidebarMenuItem key={label}>                  <SidebarMenuButton isActive={active} tooltip={label}>                    <Icon />                    <span>{label}</span>                  </SidebarMenuButton>                  {count ? <SidebarMenuBadge>{count}</SidebarMenuBadge> : null}                </SidebarMenuItem>              ))}              <SidebarMenuItem>                <SidebarMenuButton tooltip="Districts">                  <ExternalIcon />                  <span>Districts</span>                </SidebarMenuButton>                <SidebarMenuSub>                  <SidebarMenuSubItem>                    <SidebarMenuSubButton href="#harbor" isActive>                      Harbor                    </SidebarMenuSubButton>                  </SidebarMenuSubItem>                  <SidebarMenuSubItem>                    <SidebarMenuSubButton href="#rail-yards">                      Rail yards                    </SidebarMenuSubButton>                  </SidebarMenuSubItem>                </SidebarMenuSub>              </SidebarMenuItem>            </SidebarMenu>          </SidebarGroup>        </SidebarContent>        <SidebarSeparator />        <SidebarFooter>          <SidebarMenu>            <SidebarMenuItem>              <SidebarMenuButton tooltip="Help">                <InfoIcon />                <span>Help</span>              </SidebarMenuButton>            </SidebarMenuItem>          </SidebarMenu>        </SidebarFooter>      </Sidebar>      <SidebarInset render={<div />} className="gap-4 p-4">        <div className="flex items-center gap-3">          <SidebarTrigger />          <span className="font-black font-zero text-zero-lg italic">            Board          </span>        </div>        <p className="font-medium font-zero-text text-zero-sm text-zero-text-2">          Press the button or ⌘B / Ctrl+B to fold the sidebar to its icons.        </p>      </SidebarInset>    </SidebarProvider>  );}

Against the edge, floating, inset

Board

The floating sidebar.

sidebar-variants.tsx
import { useState } from "react";import { InfoIcon, LockIcon, SearchIcon } from "@/components/zero/icons";import {  Sidebar,  SidebarContent,  SidebarGroup,  SidebarGroupLabel,  SidebarInset,  SidebarMenu,  SidebarMenuButton,  SidebarMenuItem,  SidebarProvider,  SidebarTrigger,} from "@/components/zero/sidebar";import { ToggleGroup, ToggleGroupItem } from "@/components/zero/toggle-group";type Variant = "sidebar" | "floating" | "inset";const rows = [  { label: "Board", icon: SearchIcon, active: true },  { label: "Locked", icon: LockIcon },  { label: "Help", icon: InfoIcon },];export default function SidebarVariants() {  const [variant, setVariant] = useState<Variant>("floating");  return (    <div className="grid w-full gap-4">      <ToggleGroup        size="sm"        variant="secondary"        value={[variant]}        onValueChange={(next) => next[0] && setVariant(next[0] as Variant)}      >        <ToggleGroupItem value="sidebar">Sidebar</ToggleGroupItem>        <ToggleGroupItem value="floating">Floating</ToggleGroupItem>        <ToggleGroupItem value="inset">Inset</ToggleGroupItem>      </ToggleGroup>      {/* A box instead of a whole page: the sidebar's container is absolute here, not fixed. */}      <SidebarProvider className="relative h-80 min-h-0 w-full overflow-hidden border-(length:--zero-stroke-thin) border-zero-line">        <Sidebar          variant={variant}          collapsible="icon"          className="absolute h-full"        >          <SidebarContent>            <SidebarGroup>              <SidebarGroupLabel>Jobs</SidebarGroupLabel>              <SidebarMenu>                {rows.map(({ label, icon: Icon, active }) => (                  <SidebarMenuItem key={label}>                    <SidebarMenuButton isActive={active} tooltip={label}>                      <Icon />                      <span>{label}</span>                    </SidebarMenuButton>                  </SidebarMenuItem>                ))}              </SidebarMenu>            </SidebarGroup>          </SidebarContent>        </Sidebar>        <SidebarInset render={<div />} className="gap-3 p-4">          <div className="flex items-center gap-3">            <SidebarTrigger />            <span className="font-black font-zero text-zero-lg italic">              Board            </span>          </div>          <p className="font-medium font-zero-text text-zero-sm text-zero-text-2">            The {variant} sidebar.          </p>        </SidebarInset>      </SidebarProvider>    </div>  );}

Behaviour

On a wide screen it folds away entirely (collapsible="offcanvas") or down to a rail of icons ("icon"), where each row shows only its icon and names itself in a tooltip. On a phone it opens as a Sheet. SidebarTrigger, or ⌘B / Ctrl+B, opens and closes it.

It fills the screen's height by default; to put it in a box, as in the example, give the provider a height and the sidebar className="absolute h-full". useSidebar gives the open state to anything inside the provider.

variant sets it against the edge ("sidebar"), as a panel standing a little off it ("floating"), or on the black ground with the page set in beside it as a panel ("inset", with SidebarInset).

Props

SidebarProvider

PropTypeDefaultDescription
defaultOpenbooleantrueOpen at first, uncontrolled.
open / onOpenChangeboolean / (open) => void—Open, controlled.

Sidebar

PropTypeDefaultDescription
variant"sidebar" | "floating" | "inset""sidebar"Against the edge, a panel off the edge, or the page set in as a panel.
collapsible"offcanvas" | "icon" | "none""offcanvas"What closing does on a wide screen.
side"left" | "right""left"The edge it sits on.

SidebarMenuButton

PropTypeDefaultDescription
isActivebooleanfalseThe page you are on: filled with the accent.
tooltipstring—Names the row while the sidebar is folded to icons.
renderReactElement—The element to render as, such as a link.

SidebarHeader, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuItem, SidebarMenuBadge, SidebarMenuSub, SidebarMenuSubButton, SidebarSeparator, SidebarTrigger, SidebarInset