component — navigation

Sidebar

Full-featured, collapsible navigation surface built on react-aria-components and styled with the quebi design system. Supports docked/hidden collapse, float and inset intents, sections, disclosure groups, badges, tooltips, a mobile sheet, and a keyboard shortcut to toggle. The surface is positioned fixed to the viewport, next to an in-flow spacer that reserves its width — which is what a full-page app shell wants. To embed one in a sub-region of a page instead, give the wrapping element contain: layout (Tailwind's contain-layout) so it becomes the containing block for the fixed surface; otherwise the items paint at the left edge of the window rather than inside the region.

  • navigation
  • sidebar
  • nav
  • menu
  • layout
  • collapsible
  • interactive

04 examples

Default

A standard sidebar with a header, sections, an active item, and a footer.

Main content area.

Collapsible to dock

Use the trigger (or Cmd/Ctrl+B) to collapse into an icon-only dock rail.

Toggle the rail with the button.

Disclosure groups

Nest collapsible groups of items with SidebarDisclosure.

Expand the groups in the sidebar.

Float intent

Set intent="float" to detach the sidebar from the page edge into a hairline-ruled box.

A floating sidebar surface.

source

sidebar.tsx

Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.

"use client"

import { ChevronDown as ChevronDownIcon, ChevronUp as ChevronUpIcon } from "lucide-react"
import { createContext, use, useCallback, useEffect, useMemo, useRef, useState } from "react"
import type {
  ButtonProps,
  DisclosureGroupProps,
  DisclosurePanelProps,
  DisclosureProps,
  LinkProps,
  LinkRenderProps,
  ModalOverlayProps,
  SeparatorProps as SidebarSeparatorProps,
} from "react-aria-components"
import {
  composeRenderProps,
  Disclosure,
  DisclosureGroup,
  DisclosurePanel,
  Header,
  Heading,
  Modal,
  ModalOverlay,
  Separator,
  Text,
  Button as Trigger,
} from "react-aria-components"
import { Button } from "@/components/button"
import { Link } from "@/components/link"
import { Tooltip, TooltipContent } from "@/components/tooltip"
import { cn } from "@/lib/utils"

/**
 * Sidebar — quebi design system
 *
 * A full-featured, collapsible navigation surface built on
 * react-aria-components. Flat: the sidebar sits on `bg-quebi-bg` and is told
 * apart from the page by a hairline edge, not a surface or a shadow — only the
 * mobile modal, which really does float, is `bg-quebi-elevated`. A hovered
 * item takes `bg-quebi-raised`; the current one is state — the `selected`
 * ground at medium weight, its icon in signal. Items take the control radius.
 * Section labels are mono eyebrows.
 *
 * Compose a `SidebarProvider` around a `Sidebar` (containing `SidebarHeader`,
 * `SidebarContent` with `SidebarSection`/`SidebarItem`, and `SidebarFooter`)
 * and a `SidebarInset` for the main content. Supports docked/hidden collapse,
 * float and inset intents, disclosure groups, badges, tooltips, and a keyboard
 * shortcut to toggle.
 *
 * The surface is `position: fixed` to the viewport, with an in-flow spacer
 * (`data-slot="sidebar-gap"`) reserving its width beside it — the shape a
 * full-page app shell wants. Mounted in a sub-region of a page instead (a card,
 * a panel, a split view), the fixed surface resolves against the viewport and
 * paints at the left edge of the window. Give the wrapping element
 * `contain: layout` (Tailwind's `contain-layout`) so it becomes the containing
 * block for the fixed surface; `overflow-hidden` then clips it, and `h-full` on
 * the `SidebarProvider` keeps the row that element's height rather than the
 * spacer's `100svh`.
 */

const SIDEBAR_WIDTH = "17rem"
const SIDEBAR_WIDTH_DOCK = "3.25rem"
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7

// Small inline mobile detector so the component stays self-contained.
function useIsMobile(breakpoint = 768) {
  const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined)

  useEffect(() => {
    if (typeof window === "undefined") {
      return
    }
    const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`)
    const onChange = () => setIsMobile(mql.matches)
    onChange()
    mql.addEventListener("change", onChange)
    return () => mql.removeEventListener("change", onChange)
  }, [breakpoint])

  return isMobile
}

type SidebarContextProps = {
  state: "expanded" | "collapsed"
  open: boolean
  setOpen: (open: boolean) => void
  isOpenOnMobile: boolean
  setIsOpenOnMobile: (open: boolean) => void
  isMobile: boolean
  toggleSidebar: () => void
}

const SidebarContext = createContext<SidebarContextProps | null>(null)

const useSidebar = () => {
  const context = use(SidebarContext)
  if (!context) {
    throw new Error("useSidebar must be used within a SidebarProvider.")
  }

  return context
}

interface SidebarProviderProps extends React.ComponentProps<"div"> {
  defaultOpen?: boolean
  isOpen?: boolean
  shortcut?: string
  onOpenChange?: (open: boolean) => void
}

const SidebarProvider = ({
  defaultOpen = true,
  isOpen: openProp,
  onOpenChange: setOpenProp,
  className,
  style,
  children,
  shortcut = "b",
  ref,
  ...props
}: SidebarProviderProps) => {
  const [openMobile, setOpenMobile] = useState(false)

  const [internalOpenState, setInternalOpenState] = useState(defaultOpen)
  const open = openProp ?? internalOpenState
  const setOpen = useCallback(
    (value: boolean | ((value: boolean) => boolean)) => {
      const openState = typeof value === "function" ? value(open) : value

      if (setOpenProp) {
        setOpenProp(openState)
      } else {
        setInternalOpenState(openState)
      }

      // biome-ignore lint/suspicious/noDocumentCookie: the Cookie Store API the rule points at is unavailable in Safari and Firefox, and this cookie is read by the server on the next request to render the sidebar open or closed without a flash — so it has to be a real document cookie, written synchronously.
      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
    },
    [setOpenProp, open],
  )

  const isMobile = useIsMobile()
  const isMobileRef = useRef(isMobile)
  isMobileRef.current = isMobile

  const toggleSidebar = useCallback(() => {
    if (isMobileRef.current) {
      setOpenMobile((prev) => !prev)
    } else {
      setOpen((prev) => !prev)
    }
  }, [setOpen])

  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === shortcut && (event.metaKey || event.ctrlKey)) {
        const activeElement = document.activeElement

        const isInTextInput =
          activeElement instanceof HTMLInputElement ||
          activeElement instanceof HTMLTextAreaElement ||
          activeElement?.getAttribute("contenteditable") === "true" ||
          activeElement?.getAttribute("role") === "textbox"

        if (!isInTextInput) {
          event.preventDefault()
          toggleSidebar()
        }
      }
    }

    window.addEventListener("keydown", handleKeyDown)
    return () => window.removeEventListener("keydown", handleKeyDown)
  }, [toggleSidebar, shortcut])

  const state = open ? "expanded" : "collapsed"

  const contextValue = useMemo<SidebarContextProps>(
    () => ({
      state,
      open,
      setOpen,
      isMobile: isMobile ?? false,
      isOpenOnMobile: openMobile,
      setIsOpenOnMobile: setOpenMobile,
      toggleSidebar,
    }),
    [state, open, setOpen, isMobile, openMobile, toggleSidebar],
  )

  return (
    <SidebarContext value={contextValue}>
      <div
        style={
          {
            "--sidebar-width": SIDEBAR_WIDTH,
            "--sidebar-width-dock": SIDEBAR_WIDTH_DOCK,
            ...style,
          } as React.CSSProperties
        }
        className={cn(
          "@container **:data-[slot=icon]:shrink-0",
          "flex w-full text-quebi-fg",
          "group/sidebar-root peer/sidebar-root has-data-[intent=inset]:bg-quebi-raised",
          className,
        )}
        ref={ref}
        {...props}
      >
        {children}
      </div>
    </SidebarContext>
  )
}

// Self-contained mobile overlay (replaces the Cellestial Sheet on mobile).
interface SidebarMobileProps extends Omit<ModalOverlayProps, "children"> {
  side?: "left" | "right"
  children?: React.ReactNode
}

const SidebarMobile = ({ side = "left", className, children, ...props }: SidebarMobileProps) => {
  return (
    <ModalOverlay
      isDismissable
      className="entering:fade-in exiting:fade-out fixed inset-0 z-50 size-full overflow-hidden bg-black/60 entering:animate-in exiting:animate-out entering:duration-300 exiting:duration-200"
      {...props}
    >
      <Modal
        data-slot="sidebar"
        data-intent="default"
        aria-label="Sidebar"
        className={composeRenderProps(className, (resolved) =>
          cn(
            "fixed inset-y-0 z-50 flex w-(--sidebar-width) flex-col bg-quebi-elevated text-quebi-fg shadow-quebi-float [--sidebar-width:18rem]",
            "border-quebi-hairline transition will-change-transform",
            side === "left" &&
              "left-0 border-r entering:slide-in-from-left exiting:slide-out-to-left",
            side === "right" &&
              "right-0 border-l entering:slide-in-from-right exiting:slide-out-to-right",
            "entering:animate-in exiting:animate-out entering:duration-300 exiting:duration-200",
            resolved,
          ),
        )}
      >
        {children}
      </Modal>
    </ModalOverlay>
  )
}

interface SidebarProps extends React.ComponentProps<"div"> {
  intent?: "default" | "float" | "inset"
  collapsible?: "hidden" | "dock" | "none"
  side?: "left" | "right"
  closeButton?: boolean
}

const Sidebar = ({
  children,
  closeButton = true,
  collapsible = "hidden",
  side = "left",
  intent = "default",
  className,
  ...props
}: SidebarProps) => {
  const { isMobile, state, isOpenOnMobile, setIsOpenOnMobile } = useSidebar()
  if (collapsible === "none") {
    return (
      <div
        data-intent={intent}
        data-collapsible="none"
        data-slot="sidebar"
        className={cn(
          "flex h-full w-(--sidebar-width) flex-col border-quebi-hairline bg-quebi-bg text-quebi-fg",
          side === "right" ? "border-s" : "border-e",
          className,
        )}
        {...props}
      >
        {children}
      </div>
    )
  }

  if (isMobile) {
    return (
      <>
        <span className="sr-only" aria-hidden data-intent={intent} />
        <SidebarMobile isOpen={isOpenOnMobile} onOpenChange={setIsOpenOnMobile} side={side}>
          {children}
        </SidebarMobile>
      </>
    )
  }

  return (
    <div
      data-state={state}
      data-collapsible={state === "collapsed" ? collapsible : ""}
      data-intent={intent}
      data-side={side}
      data-slot="sidebar"
      className="group peer hidden text-quebi-fg md:block"
      {...props}
    >
      <div
        data-slot="sidebar-gap"
        aria-hidden="true"
        className={cn([
          "w-(--sidebar-width) group-data-[collapsible=hidden]:w-0",
          "group-data-[side=right]:rotate-180",
          "relative h-svh bg-transparent transition-[width] duration-200 ease-linear",
          intent === "default" && "group-data-[collapsible=dock]:w-(--sidebar-width-dock)",
          intent === "float" &&
            "group-data-[collapsible=dock]:w-[calc(var(--sidebar-width-dock)+--spacing(4))]",
          intent === "inset" &&
            "group-data-[collapsible=dock]:w-[calc(var(--sidebar-width-dock)+--spacing(2))]",
        ])}
      />
      <div
        data-slot="sidebar-container"
        className={cn(
          "fixed inset-y-0 z-10 hidden w-(--sidebar-width) bg-quebi-bg not-has-data-[slot=sidebar-footer]:pb-2 md:flex",
          "transition-[left,right,width] duration-200 ease-linear",
          side === "left" &&
            "left-0 group-data-[collapsible=hidden]:left-[calc(var(--sidebar-width)*-1)]",
          side === "right" &&
            "right-0 group-data-[collapsible=hidden]:right-[calc(var(--sidebar-width)*-1)]",
          intent === "float" && "p-2 group-data-[collapsible=dock]:w-[calc(--spacing(4)+2px)]",
          intent === "inset" && [
            "bg-transparent",
            "group-data-[collapsible=dock]:w-[calc(var(--sidebar-width-dock)+--spacing(2)+2px)]",
          ],
          intent === "default" && [
            "group-data-[collapsible=dock]:w-(--sidebar-width-dock)",
            "border-quebi-hairline group-data-[side=left]:border-r group-data-[side=right]:border-l",
          ],
          className,
        )}
        {...props}
      >
        <div
          data-sidebar="default"
          data-slot="sidebar-inner"
          className={cn(
            "flex h-full w-full flex-col text-quebi-fg",
            "group-data-[intent=float]:border group-data-[intent=float]:border-quebi-hairline",
          )}
        >
          {children}
        </div>
      </div>
    </div>
  )
}

const SidebarHeader = ({ className, ref, ...props }: React.ComponentProps<"div">) => {
  const { state } = useSidebar()
  return (
    <div
      ref={ref}
      data-slot="sidebar-header"
      className={cn(
        "flex flex-col gap-2 p-2.5 [.border-b]:border-quebi-hairline",
        "in-data-[intent=inset]:p-4",
        state === "collapsed" ? "items-center p-2.5" : "p-4",
        className,
      )}
      {...props}
    />
  )
}

const SidebarFooter = ({ className, ...props }: React.ComponentProps<"div">) => {
  return (
    <div
      data-slot="sidebar-footer"
      className={cn([
        "mt-auto flex shrink-0 items-center justify-center p-4 **:data-[slot=chevron]:text-quebi-fg-muted",
        "in-data-[intent=inset]:px-6 in-data-[intent=inset]:py-4",
        className,
      ])}
      {...props}
    />
  )
}

const SidebarContent = ({ className, ...props }: React.ComponentProps<"div">) => {
  const { state } = useSidebar()
  return (
    <div
      data-slot="sidebar-content"
      className={cn(
        "flex min-h-0 flex-1 scroll-mb-96 flex-col overflow-auto *:data-[slot=sidebar-section]:border-l-0",
        state === "collapsed" ? "items-center" : "mask-b-from-95%",
        className,
      )}
      {...props}
    >
      {props.children}
    </div>
  )
}

const SidebarSectionGroup = ({ className, ...props }: React.ComponentProps<"section">) => {
  const { state, isMobile } = useSidebar()
  const collapsed = state === "collapsed" && !isMobile
  return (
    <section
      data-slot="sidebar-section-group"
      className={cn(
        "flex w-full min-w-0 flex-col gap-y-0.5",
        collapsed && "items-center justify-center",
        className,
      )}
      {...props}
    />
  )
}

interface SidebarSectionProps extends React.ComponentProps<"div"> {
  label?: string
}

const SidebarSection = ({ className, ...props }: SidebarSectionProps) => {
  const { state } = useSidebar()
  return (
    <div
      data-slot="sidebar-section"
      className={cn(
        "col-span-full flex min-w-0 flex-col gap-y-0.5 **:data-[slot=sidebar-section]:**:gap-y-0",
        "in-data-[state=collapsed]:p-2 px-4 py-2",
        className,
      )}
      {...props}
    >
      {state !== "collapsed" && "label" in props && (
        <Header className="quebi-eyebrow mt-2 mb-2 shrink-0 px-2 outline-none transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=dock]:-mt-8 group-data-[collapsible=dock]:opacity-0">
          {props.label}
        </Header>
      )}
      <div
        data-slot="sidebar-section-inner"
        className="grid grid-cols-[auto_1fr] gap-y-0.5 in-data-[state=collapsed]:gap-y-1.5 *:data-[slot=control]:col-span-full"
      >
        {props.children}
      </div>
    </div>
  )
}

interface SidebarItemProps extends Omit<React.ComponentProps<typeof Link>, "children"> {
  isCurrent?: boolean
  children?:
    | React.ReactNode
    | ((
        values: LinkRenderProps & { defaultChildren: React.ReactNode; isCollapsed: boolean },
      ) => React.ReactNode)
  badge?: string | number | undefined
  tooltip?: string | React.ComponentProps<typeof TooltipContent>
}

const SidebarItem = ({
  isCurrent,
  tooltip,
  children,
  badge,
  className,
  ref,
  ...props
}: SidebarItemProps) => {
  const { state, isMobile } = useSidebar()
  const isCollapsed = state === "collapsed" && !isMobile
  const link = (
    <Link
      ref={ref}
      data-slot="sidebar-item"
      aria-current={isCurrent ? "page" : undefined}
      className={composeRenderProps(
        className,
        (className, { isPressed, isFocusVisible, isHovered, isDisabled }) =>
          cn([
            "href" in props ? "cursor-pointer" : "cursor-default",
            "w-full min-w-0 items-center text-start text-base/6 text-quebi-fg-muted no-underline transition-colors duration-150 hover:no-underline",
            "group/sidebar-item relative col-span-full overflow-hidden rounded-(--q-radius-control) focus-visible:outline-hidden",
            "grid grid-cols-[auto_1fr_1.5rem_0.5rem_auto] **:last:data-[slot=icon]:ms-auto supports-[grid-template-columns:subgrid]:grid-cols-subgrid sm:text-sm/5",
            "p-2 has-[a]:p-0",
            // icon
            "**:data-[slot=icon]:shrink-0 [&_[data-slot='icon']:not([class*='size-'])]:size-5 sm:[&_[data-slot='icon']:not([class*='size-'])]:size-4 [&_[data-slot='icon']:not([class*='text-'])]:text-quebi-fg-muted",
            "**:last:data-[slot=icon]:size-5 sm:**:last:data-[slot=icon]:size-4",
            "[&:has([data-slot=icon]+[data-slot=sidebar-label])_[data-slot=icon]:has(+[data-slot=sidebar-label])]:me-2",

            // avatar
            "**:data-[slot=avatar]:[--avatar-size:--spacing(5)]",
            "[&:has([data-slot=avatar]+[data-slot=sidebar-label])_[data-slot=avatar]:has(+[data-slot=sidebar-label])]:me-2",
            // Current: state — the selected ground at medium weight, the icon in signal.
            isCurrent &&
              "bg-quebi-selected font-medium text-quebi-on-selected [&_[data-slot='icon']:not([class*='text-'])]:text-quebi-signal",
            isFocusVisible &&
              "outline-hidden ring-2 ring-quebi-focus ring-inset",
            !isCurrent &&
              (isPressed || isHovered) &&
              "bg-quebi-raised text-quebi-fg [&_[data-slot='icon']:not([class*='text-'])]:text-quebi-fg",
            isDisabled && "opacity-50",
            className,
          ]),
      )}
      {...props}
    >
      {(values) => (
        <>
          {typeof children === "function" ? children({ ...values, isCollapsed }) : children}

          {badge &&
            (state !== "collapsed" ? (
              <span
                data-slot="sidebar-badge"
                className="absolute inset-y-1/2 end-1.5 h-5.5 w-auto -translate-y-1/2 rounded-full bg-quebi-raised px-2 font-mono text-quebi-label/5.5 text-quebi-fg-muted group-hover/sidebar-item:bg-quebi-pressed"
              >
                {badge}
              </span>
            ) : (
              <div
                aria-hidden
                className="absolute end-1 top-1 size-1.5 rounded-full bg-quebi-action"
              />
            ))}
        </>
      )}
    </Link>
  )
  if (typeof tooltip === "string") {
    tooltip = {
      children: tooltip,
    }
  }

  return (
    <Tooltip delay={0}>
      {link}
      <TooltipContent
        className="**:data-[slot=icon]:hidden **:data-[slot=sidebar-label-mask]:hidden"
        placement="right"
        arrow
        hidden={!isCollapsed || isMobile || !tooltip}
        {...tooltip}
      />
    </Tooltip>
  )
}

interface SidebarLinkProps extends LinkProps {
  ref?: React.RefObject<HTMLAnchorElement>
}

const SidebarLink = ({ className, ref, ...props }: SidebarLinkProps) => {
  return (
    <Link
      ref={ref}
      className={composeRenderProps(className, (resolved) =>
        cn(
          "col-span-full min-w-0 shrink-0 items-center p-2 focus:outline-hidden",
          "grid grid-cols-[auto_1fr_1.5rem_0.5rem_auto] supports-[grid-template-columns:subgrid]:grid-cols-subgrid",
          resolved,
        ),
      )}
      {...props}
    />
  )
}

const SidebarInset = ({ className, ref, ...props }: React.ComponentProps<"main">) => {
  return (
    <main
      data-slot="sidebar-inset"
      ref={ref}
      className={cn(
        "relative flex w-full flex-1 flex-col bg-quebi-bg lg:min-w-0",
        "group-has-data-[intent=inset]/sidebar-root:border group-has-data-[intent=inset]/sidebar-root:border-quebi-hairline",
        "md:group-has-data-[intent=inset]/sidebar-root:m-2",
        "md:group-has-data-[side=left]:group-has-data-[intent=inset]/sidebar-root:ms-0",
        "md:group-has-data-[side=right]:group-has-data-[intent=inset]/sidebar-root:me-0",
        "md:group-has-data-[intent=inset]/sidebar-root:peer-data-[state=collapsed]:ms-2",
        className,
      )}
      {...props}
    />
  )
}

type SidebarDisclosureGroupProps = DisclosureGroupProps
const SidebarDisclosureGroup = ({
  allowsMultipleExpanded = true,
  className,
  ...props
}: SidebarDisclosureGroupProps) => {
  return (
    <DisclosureGroup
      data-slot="sidebar-disclosure-group"
      allowsMultipleExpanded={allowsMultipleExpanded}
      className={composeRenderProps(className, (resolved) =>
        cn(
          "col-span-full flex min-w-0 flex-col gap-y-0.5 in-data-[state=collapsed]:gap-y-1.5",
          resolved,
        ),
      )}
      {...props}
    />
  )
}

interface SidebarDisclosureProps extends DisclosureProps {
  ref?: React.Ref<HTMLDivElement>
}

const SidebarDisclosure = ({ className, ref, ...props }: SidebarDisclosureProps) => {
  return (
    <Disclosure
      ref={ref}
      data-slot="sidebar-disclosure"
      className={composeRenderProps(className, (resolved) => cn("col-span-full min-w-0", resolved))}
      {...props}
    />
  )
}

interface SidebarDisclosureTriggerProps extends ButtonProps {
  ref?: React.Ref<HTMLButtonElement>
  tooltip?: string | React.ComponentProps<typeof TooltipContent>
}

const SidebarDisclosureTrigger = ({
  className,
  ref,
  tooltip,
  ...props
}: SidebarDisclosureTriggerProps) => {
  const { state, isMobile } = useSidebar()
  const isCollapsed = state === "collapsed" && !isMobile
  const trigger = (
    <Heading level={3}>
      <Trigger
        ref={ref}
        slot="trigger"
        className={composeRenderProps(
          className,
          (className, { isPressed, isFocusVisible, isHovered, isDisabled }) =>
            cn(
              "flex w-full min-w-0 items-center text-start font-medium text-base/6 text-quebi-fg transition-colors duration-150",
              "group/sidebar-disclosure-trigger relative col-span-full overflow-hidden rounded-(--q-radius-control) focus-visible:outline-hidden",
              "**:data-[slot=icon]:size-5 **:data-[slot=icon]:shrink-0 **:data-[slot=icon]:text-quebi-fg-muted sm:**:data-[slot=icon]:size-4",
              "**:last:data-[slot=icon]:size-5 sm:**:last:data-[slot=icon]:size-4",
              "**:data-[slot=avatar]:size-6 sm:**:data-[slot=avatar]:size-5",
              "col-span-full gap-3 p-2 **:data-[slot=chevron]:text-quebi-fg-muted **:last:data-[slot=icon]:ms-auto sm:gap-2 sm:text-sm/5",
              isCollapsed && "justify-center",

              isFocusVisible && "outline-hidden ring-2 ring-quebi-focus ring-inset",
              (isPressed || isHovered) &&
                "bg-quebi-raised text-quebi-fg **:data-[slot=chevron]:text-quebi-fg **:data-[slot=icon]:text-quebi-fg **:last:data-[slot=icon]:text-quebi-fg",
              isDisabled && "opacity-50",
              className,
            ),
        )}
        {...props}
      >
        {(values) => (
          <>
            {typeof props.children === "function" ? props.children(values) : props.children}
            <ChevronDownIcon
              data-slot="chevron"
              className={cn(
                "z-10 size-3.5 group-aria-expanded/sidebar-disclosure-trigger:hidden",
                !isCollapsed && "ms-auto",
              )}
            />
            <ChevronUpIcon
              data-slot="chevron"
              className={cn(
                "z-10 hidden size-3.5 group-aria-expanded/sidebar-disclosure-trigger:block",
                !isCollapsed && "ms-auto",
              )}
            />
          </>
        )}
      </Trigger>
    </Heading>
  )
  if (typeof tooltip === "string") {
    tooltip = { children: tooltip }
  }
  return (
    <Tooltip delay={0}>
      {trigger}
      <TooltipContent
        className="**:data-[slot=icon]:hidden **:data-[slot=sidebar-label-mask]:hidden"
        placement="right"
        arrow
        hidden={!isCollapsed || isMobile || !tooltip}
        {...tooltip}
      />
    </Tooltip>
  )
}

const SidebarDisclosurePanel = ({ className, ...props }: DisclosurePanelProps) => {
  return (
    <DisclosurePanel
      data-slot="sidebar-disclosure-panel"
      className={composeRenderProps(className, (resolved) =>
        cn(
          "h-(--disclosure-panel-height) overflow-clip transition-[height] duration-200",
          resolved,
        ),
      )}
      {...props}
    >
      <div
        data-slot="sidebar-disclosure-panel-content"
        className="col-span-full grid grid-cols-[auto_1fr] gap-y-0.5"
      >
        {props.children}
      </div>
    </DisclosurePanel>
  )
}

const SidebarSeparator = ({ className, ...props }: SidebarSeparatorProps) => {
  return (
    <Separator
      data-slot="sidebar-separator"
      orientation="horizontal"
      className={cn(
        "mx-auto h-px w-[calc(var(--sidebar-width)---spacing(10))] border-0 bg-quebi-hairline forced-colors:bg-[ButtonBorder]",
        className,
      )}
      {...props}
    />
  )
}

const SidebarTrigger = ({
  onPress,
  className,
  children,
  ...props
}: React.ComponentProps<typeof Button>) => {
  const { toggleSidebar } = useSidebar()
  return (
    <Button
      aria-label={props["aria-label"] || "Toggle Sidebar"}
      data-slot="sidebar-trigger"
      intent={props.intent || "ghost"}
      size={props.size || "sq-sm"}
      className={composeRenderProps(className, (resolved) => cn("shrink-0", resolved))}
      onPress={(event) => {
        onPress?.(event)
        toggleSidebar()
      }}
      {...props}
    >
      {children || (
        <>
          <svg
            aria-hidden="true"
            data-slot="icon"
            className="size-4"
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 16 16"
            width={16}
            height={16}
            fill="currentcolor"
          >
            <path d="M13.25 2.5c.69 0 1.25.56 1.25 1.25v8.5c0 .69-.56 1.25-1.25 1.25H7.5V15h5.75A2.75 2.75 0 0 0 16 12.25v-8.5A2.75 2.75 0 0 0 13.25 1H7.5v1.5zM5.75 1a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-3A2.75 2.75 0 0 1 0 12.25v-8.5A2.75 2.75 0 0 1 2.75 1z" />
          </svg>
          <span className="sr-only">Toggle Sidebar</span>
        </>
      )}
    </Button>
  )
}

// Typed as a RAC Button, not a DOM button: the props are spread onto <Trigger>
// (react-aria's Button), which takes onPress rather than onClick.
const SidebarRail = ({ className, ref, ...props }: ButtonProps & { ref?: React.Ref<HTMLButtonElement> }) => {
  const { toggleSidebar } = useSidebar()

  return !props.children ? (
    <Trigger
      ref={ref}
      data-slot="sidebar-rail"
      aria-label="Toggle Sidebar"
      // react-aria's equivalent of tabIndex={-1}: the rail is a pointer
      // affordance, reachable by keyboard through the sidebar trigger instead.
      excludeFromTabOrder
      onPress={toggleSidebar}
      className={composeRenderProps(className, (resolved) =>
        cn(
          "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 outline-hidden transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-0.5 hover:after:bg-transparent group-data-[side=left]:-right-4 group-data-[side=right]:left-0 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=hidden]:translate-x-0 group-data-[collapsible=hidden]:hover:bg-quebi-raised group-data-[collapsible=hidden]:after:left-full",
          "[[data-side=left][data-collapsible=hidden]_&]:-right-2 [[data-side=right][data-collapsible=hidden]_&]:-left-2",
          resolved,
        ),
      )}
      {...props}
    />
  ) : (
    props.children
  )
}

const SidebarLabel = ({ className, ref, ...props }: React.ComponentProps<typeof Text>) => {
  const { state, isMobile } = useSidebar()
  const collapsed = state === "collapsed" && !isMobile
  if (!collapsed) {
    return (
      <Text
        data-slot="sidebar-label"
        tabIndex={-1}
        ref={ref}
        slot="label"
        className={cn("col-start-2 truncate pe-6 outline-hidden", className)}
        {...props}
      >
        {props.children}
      </Text>
    )
  }
  return null
}

interface SidebarNavProps extends React.ComponentProps<"nav"> {
  isSticky?: boolean
}

const SidebarNav = ({ isSticky = false, className, ...props }: SidebarNavProps) => {
  return (
    <nav
      data-slot="sidebar-nav"
      className={cn(
        "isolate flex items-center justify-between gap-x-2 px-(--container-padding,--spacing(4)) py-2.5 text-quebi-fg sm:justify-start sm:px-(--gutter,--spacing(4)) md:w-full",
        isSticky && "static top-0 z-40 group-has-data-[intent=default]/sidebar-root:sticky",
        className,
      )}
      {...props}
    />
  )
}

interface SidebarMenuTriggerProps extends ButtonProps {
  alwaysVisible?: boolean
}
const SidebarMenuTrigger = ({
  alwaysVisible = false,
  className,
  ...props
}: SidebarMenuTriggerProps) => {
  return (
    <Trigger
      className={composeRenderProps(className, (resolved) =>
        cn(
          !alwaysVisible &&
            "opacity-0 pressed:opacity-100 group-hover/sidebar-item:opacity-100 group-focus-visible/sidebar-item:opacity-100 group/sidebar-item:pressed:opacity-100",
          "absolute end-0 flex h-full w-[calc(var(--sidebar-width)-90%)] items-center justify-end pe-2.5 outline-hidden",
          "**:data-[slot=icon]:shrink-0 [&_[data-slot='icon']:not([class*='size-'])]:size-5 sm:[&_[data-slot='icon']:not([class*='size-'])]:size-4 pressed:[&_[data-slot='icon']:not([class*='text-'])]:text-quebi-fg",
          "pressed:text-quebi-fg text-quebi-fg-muted hover:text-quebi-fg",
          resolved,
        ),
      )}
      {...props}
    />
  )
}

export type {
  SidebarDisclosureGroupProps,
  SidebarDisclosureProps,
  SidebarDisclosureTriggerProps,
  SidebarItemProps,
  SidebarLinkProps,
  SidebarNavProps,
  SidebarProps,
  SidebarProviderProps,
  SidebarSectionProps,
  SidebarSeparatorProps,
}

export {
  Sidebar,
  SidebarContent,
  SidebarDisclosure,
  SidebarDisclosureGroup,
  SidebarDisclosurePanel,
  SidebarDisclosureTrigger,
  SidebarFooter,
  SidebarHeader,
  SidebarInset,
  SidebarItem,
  SidebarLabel,
  SidebarLink,
  SidebarMenuTrigger,
  SidebarNav,
  SidebarProvider,
  SidebarRail,
  SidebarSection,
  SidebarSectionGroup,
  SidebarSeparator,
  SidebarTrigger,
  useSidebar,
}