MC Sidebar

A sidebar navigation component.

Overview

The mc-sidebar is a layout and navigation system for MicroClub UI. It provides a complete sidebar architecture with provider state, collapsible behavior, grouped menus, nested navigation, and a content inset area.

Built as part of the MicroClub library, this component is designed for instant deployment into your codebase, while keeping full control over styling, structure, and interactions.

Preview

Loading...
'use client';import { useState } from 'react';import {  BookOpen,  Briefcase,  CircleDashed,  Compass,  ChevronsUpDown,  Grid2x2,  MoreHorizontal,  Settings,  Sparkles,  User,} from 'lucide-react';import MCLogo from '@/components/MCLogo';import {  McSidebar,  McSidebarContent,  McSidebarFooter,  McSidebarGroup,  McSidebarGroupContent,  McSidebarGroupLabel,  McSidebarHeader,  McSidebarInset,  McSidebarCollapsible,  McSidebarMenu,  McSidebarMenuSubButton,  McSidebarMenuSubItem,  McSidebarRail,  McSidebarTrigger,  SidebarProvider,  useSidebar,} from '../ui/mc-sidebar';const platformItems = [  { label: 'Playground', icon: Sparkles },  { label: 'Models', icon: Briefcase },  { label: 'Documentation', icon: BookOpen },  { label: 'API Reference', icon: Settings },];const projectItems = [  { label: 'Design Engineering', icon: Grid2x2 },  { label: 'Sales & Marketing', icon: CircleDashed },  { label: 'Travel', icon: Compass },  { label: 'More', icon: MoreHorizontal },];export default function McSidebarDemo() {  const [open, setOpen] = useState(true);  return (    <SidebarProvider      open={open}      onOpenChange={setOpen}      className="!h-[24rem] !min-h-0 !w-full overflow-hidden"      style={        {          '--sidebar-width': '255px',        } as React.CSSProperties      }    >      <SidebarDemoContent />    </SidebarProvider>  );}function SidebarTriggerButton() {  const { state } = useSidebar();  const baseWidth = state === 'collapsed' ? 'var(--sidebar-width-icon)' : 'var(--sidebar-width)';  const offsetValue = `calc(${baseWidth} + 26px)`;  return (    <McSidebarTrigger      className="absolute top-6 z-50 size-8 bg-transparent text-sidebar-foreground shadow-none transition-colors hover:bg-transparent hover:text-sidebar-foreground active:text-sidebar-accent-foreground"      style={{ left: offsetValue } as React.CSSProperties}    />  );}function SidebarDemoContent() {  const { state } = useSidebar();  const isCollapsed = state === 'collapsed';  const insetPaddingLeft = isCollapsed    ? 'calc(var(--sidebar-width-icon) + 3.5rem)'    : 'calc(var(--sidebar-width) + 3.5rem)';  return (    <div className="relative isolate flex h-full min-h-0 w-full rounded-xl bg-background [&_[data-slot=sidebar-gap]]:hidden overflow-hidden [transform:translateZ(0)]">      <SidebarTriggerButton />      <McSidebar        side="left"        variant="sidebar"        collapsible="icon"        className="!absolute !top-0 gap-4 !left-0 !flex !h-full !w-[var(--sidebar-width)] group-data-[collapsible=icon]:!w-[var(--sidebar-width-icon)] [&_[data-slot=sidebar-container]]:!absolute [&_[data-slot=sidebar-container]]:!top-0 [&_[data-slot=sidebar-container]]:!left-0 [&_[data-slot=sidebar-container]]:!h-[24rem] [&_[data-slot=sidebar-container]]:!z-0"      >        <McSidebarHeader className="gap-2 p-2">          <div className="flex items-center justify-between rounded-md px-1 py-1 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:pt-1 group-data-[collapsible=icon]:pb-2">            <div className="flex items-center gap-2">              <MCLogo size={34} className="shrink-0 text-sidebar-foreground" />              <div className="group-data-[collapsible=icon]:hidden gap-1">                <p className="text-sm font-semibold text-sidebar-foreground">Micro Club</p>                <p className="text-xs text-muted-foreground">Scientific club</p>              </div>            </div>            <ChevronsUpDown className="size-4 text-muted-foreground" />          </div>        </McSidebarHeader>        <McSidebarContent>          <McSidebarGroup className="p-2 group-data-[collapsible=icon]:p-1 gap-6">            <McSidebarGroupLabel className="h-4 px-1.5 text-sm">Platform</McSidebarGroupLabel>            <McSidebarGroupContent className="h-auto">              <McSidebarMenu className="gap-2">                {platformItems.map((item, index) => {                  const Icon = item.icon;                  const withChildren = index === 0;                  return (                    <McSidebarCollapsible                      key={item.label}                      label={item.label}                      icon={Icon}                      tooltip={item.label}                      defaultOpen={withChildren}                      triggerClassName="h-7 p-2"                      contentClassName="mx-2 mt-0.5 gap-2 px-1.5 py-0"                    >                      {withChildren ? (                        <>                          <McSidebarMenuSubItem>                            <McSidebarMenuSubButton                              href="#"                              className="px-1.5 no-underline hover:no-underline"                            >                              History                            </McSidebarMenuSubButton>                          </McSidebarMenuSubItem>                          <McSidebarMenuSubItem>                            <McSidebarMenuSubButton                              href="#"                              className="px-1.5 no-underline hover:no-underline"                            >                              Starred                            </McSidebarMenuSubButton>                          </McSidebarMenuSubItem>                          <McSidebarMenuSubItem>                            <McSidebarMenuSubButton                              href="#"                              className="px-1.5 no-underline hover:no-underline"                            >                              Settings                            </McSidebarMenuSubButton>                          </McSidebarMenuSubItem>                        </>                      ) : null}                    </McSidebarCollapsible>                  );                })}              </McSidebarMenu>            </McSidebarGroupContent>          </McSidebarGroup>          {[0].map((groupIndex) => (            <McSidebarGroup              key={groupIndex}              className="p-2 group-data-[collapsible=icon]:p-1 gap-6"            >              <McSidebarGroupLabel className="h-4 px-1.5 text-sm">Projects</McSidebarGroupLabel>              <McSidebarGroupContent className="h-auto">                <McSidebarMenu className="gap-2">                  {projectItems.map((item) => {                    const Icon = item.icon;                    return (                      <McSidebarCollapsible                        key={`${groupIndex}-${item.label}`}                        label={item.label}                        icon={Icon}                        tooltip={item.label}                        triggerClassName="h-7 p-2"                      />                    );                  })}                </McSidebarMenu>              </McSidebarGroupContent>            </McSidebarGroup>          ))}        </McSidebarContent>        <McSidebarFooter className="mt-auto mb-0.5 gap-0 border-t border-sidebar-border p-2">          <div className="flex w-full max-w-full items-center gap-2 rounded-md px-1 py-1 hover:bg-sidebar-accent group-data-[collapsible=icon]:justify-center">            <div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-sidebar-accent text-sidebar-foreground">              <User className="h-4 w-4" />            </div>            <div className="min-w-0 space-y-0.5 group-data-[collapsible=icon]:hidden">              <p className="truncate text-sm font-medium text-sidebar-foreground">micro@club.dev</p>              <p className="text-xs text-muted-foreground">Admin workspace</p>            </div>          </div>        </McSidebarFooter>        <McSidebarRail />      </McSidebar>      <McSidebarInset        className="flex min-h-0 flex-1 flex-col transition-[padding-left] duration-200 ease-linear"        style={{ paddingLeft: insetPaddingLeft } as React.CSSProperties}      >        <div className="sticky top-0 z-10 flex items-center justify-between border-b border-border bg-background/95 px-4 pt-4 pb-3 backdrop-blur supports-[backdrop-filter]:bg-background/80">          <div className="space-y-0">            <p className="text-sm text-muted-foreground">Preview</p>            <div className="flex items-center gap-2">              <h3 className="text-xl font-semibold text-foreground">McSidebar</h3>            </div>          </div>        </div>        <div className="flex-1 overflow-auto px-4 py-4">          <p className="max-w-sm text-sm text-muted-foreground">            Demo synchronise avec la story pour montrer le header, le contenu et le footer dans la            preview de la documentation.          </p>        </div>      </McSidebarInset>    </div>  );}

Add to Your Project

Deploy the mc-sidebar directly to your components/ui directory:

npx mcoli-ui@latest add mc-sidebar
pnpm dlx mcoli-ui@latest add mc-sidebar
yarn dlx mcoli-ui@latest add mc-sidebar
bunx mcoli-ui@latest add mc-sidebar

Manual Setup

If you prefer manual control:

Install the core dependencies
npm install @base-ui/react lucide-react
pnpm add @base-ui/react lucide-react
yarn add @base-ui/react lucide-react
bun add @base-ui/react lucide-react
Copy the source code
'use client';import * as React from 'react';import { mergeProps } from '@base-ui/react/merge-props';import { useRender } from '@base-ui/react/use-render';import { cva, type VariantProps } from 'class-variance-authority';import { ChevronDownIcon, PanelLeftIcon } from 'lucide-react';import { cn } from '@/lib/utils';import { McButton } from '@/registry/ui/mc-button';const SIDEBAR_COOKIE_NAME = 'sidebar_state';const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;const SIDEBAR_WIDTH = '255px';const SIDEBAR_WIDTH_ICON = '51px';const SIDEBAR_KEYBOARD_SHORTCUT = 'b';type SidebarContextProps = {  state: 'expanded' | 'collapsed';  open: boolean;  setOpen: (open: boolean) => void;  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,  className,  style,  children,  ...props}: React.ComponentProps<'div'> & {  defaultOpen?: boolean;  open?: boolean;  onOpenChange?: (open: boolean) => void;}) {  const [_open, _setOpen] = React.useState(() => {    if (typeof document === 'undefined') {      return defaultOpen;    }    const cookie = document.cookie      .split('; ')      .find((entry) => entry.startsWith(`${SIDEBAR_COOKIE_NAME}=`));    if (!cookie) {      return defaultOpen;    }    return cookie.split('=')[1] === 'true';  });  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]  );  React.useEffect(() => {    document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;  }, [open]);  const toggleSidebar = React.useCallback(() => {    return setOpen((open) => !open);  }, [setOpen]);  React.useEffect(() => {    const handleKeyDown = (event: KeyboardEvent) => {      if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {        event.preventDefault();        toggleSidebar();      }    };    window.addEventListener('keydown', handleKeyDown);    return () => window.removeEventListener('keydown', handleKeyDown);  }, [toggleSidebar]);  const state = open ? 'expanded' : 'collapsed';  const contextValue = React.useMemo<SidebarContextProps>(    () => ({      state,      open,      setOpen,      toggleSidebar,    }),    [state, open, setOpen, toggleSidebar]  );  return (    <SidebarContext.Provider value={contextValue}>      <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>    </SidebarContext.Provider>  );}function McSidebar({  side = 'left',  variant = 'sidebar',  collapsible = 'icon',  className,  children,  ...props}: React.ComponentProps<'div'> & {  side?: 'left' | 'right';  variant?: 'sidebar' | 'floating' | 'inset';  collapsible?: 'offcanvas' | 'icon' | 'none';}) {  const { state } = useSidebar();  return (    <div      className={cn(        'group peer hidden text-sidebar-foreground md:flex',        'data-[side=right]:flex-row-reverse'      )}      data-state={collapsible === 'none' ? 'expanded' : state}      data-collapsible={collapsible === 'none' ? '' : state === 'collapsed' ? collapsible : ''}      data-variant={variant}      data-side={side}      data-slot="sidebar"    >      <div        data-slot="sidebar-gap"        className={cn(          'relative w-0 bg-transparent transition-[width] duration-200 ease-linear',          'group-data-[side=left]:w-[calc(var(--sidebar-width)+1.25rem)]',          'group-data-[side=right]:w-[calc(var(--sidebar-width)+1.25rem)]',          'group-data-[collapsible=offcanvas]:w-0',          'group-data-[collapsible=icon]:w-0',          'group-data-[side=left]:group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+1.25rem)]',          'group-data-[side=right]:group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+1.25rem)]'        )}      />      <div        data-slot="sidebar-container"        data-side={side}        className={cn(          'fixed top-5 z-10 hidden h-[calc(100svh-40px)] w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:left-5 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-5 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex',          'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',          className        )}        {...props}      >        <div          data-sidebar="sidebar"          data-slot="sidebar-inner"          className={cn(            'flex size-full min-h-0 flex-col overflow-hidden rounded-lg border border-sidebar-border bg-sidebar',            'group-data-[variant=floating]:shadow-lg group-data-[variant=floating]:shadow-sidebar/10',            'group-data-[variant=inset]:bg-sidebar/95 group-data-[variant=inset]:backdrop-blur-sm'          )}        >          {children}        </div>      </div>    </div>  );}function McSidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof McButton>) {  const { toggleSidebar } = useSidebar();  const handleClick: React.ComponentProps<typeof McButton>['onClick'] = (event) => {    onClick?.(event);    toggleSidebar();  };  return (    <McButton      type="button"      data-sidebar="trigger"      data-slot="sidebar-trigger"      variant="tertiary"      size="sm"      className={cn('h-8 w-8 p-0', className)}      onClick={handleClick}      {...props}    >      <PanelLeftIcon className="cn-rtl-flip" />      <span className="sr-only">Toggle Sidebar</span>    </McButton>  );}function McSidebarRail({ className, ...props }: React.ComponentProps<'button'>) {  const { toggleSidebar } = useSidebar();  return (    <button      type="button"      data-sidebar="rail"      data-slot="sidebar-rail"      aria-label="Toggle Sidebar"      onClick={toggleSidebar}      title="Toggle Sidebar"      className={cn(        'absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2',        '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 McSidebarInset({ className, ...props }: React.ComponentProps<'main'>) {  return (    <main      data-slot="sidebar-inset"      className={cn(        'relative flex min-h-0 w-full flex-1 flex-col overflow-y-auto bg-background md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm',        className      )}      {...props}    />  );}function McSidebarHeader({ className, ...props }: React.ComponentProps<'div'>) {  return (    <div      data-slot="sidebar-header"      data-sidebar="header"      className={cn(        'shrink-0 flex flex-col gap-2 p-2 group-data-[collapsible=icon]:p-1',        className      )}      {...props}    />  );}function McSidebarFooter({ className, ...props }: React.ComponentProps<'div'>) {  return (    <div      data-slot="sidebar-footer"      data-sidebar="footer"      className={cn(        'shrink-0 flex flex-col gap-2 p-2 group-data-[collapsible=icon]:p-1',        className      )}      {...props}    />  );}function McSidebarSeparator({ className, ...props }: React.ComponentProps<'div'>) {  return (    <div      data-slot="sidebar-separator"      data-sidebar="separator"      className={cn(        'mx-2 h-px w-auto shrink-0 bg-sidebar-border group-data-[collapsible=icon]:mx-1',        className      )}      {...props}    />  );}function McSidebarContent({ className, ...props }: React.ComponentProps<'div'>) {  return (    <div      data-slot="sidebar-content"      data-sidebar="content"      className={cn(        'no-scrollbar flex min-h-0 flex-1 flex-col gap-0 overflow-x-hidden overflow-y-auto overscroll-contain group-data-[collapsible=icon]:overflow-hidden',        className      )}      {...props}    />  );}function McSidebarGroup({ 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 group-data-[collapsible=icon]:p-1',        className      )}      {...props}    />  );}function McSidebarGroupLabel({  className,  render,  ...props}: useRender.ComponentProps<'div'> & React.ComponentProps<'div'>) {  return useRender({    defaultTagName: 'div',    props: mergeProps<'div'>(      {        className: cn(          'flex h-8 w-full shrink-0 items-center gap-2 rounded-md px-2 text-xs leading-4 font-normal tracking-normal text-sidebar-foreground opacity-70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 group-data-[collapsible=icon]:hidden focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',          className        ),      },      props    ),    render,    state: {      slot: 'sidebar-group-label',      sidebar: 'group-label',    },  });}function McSidebarGroupAction({  className,  render,  ...props}: useRender.ComponentProps<'button'> & React.ComponentProps<'button'>) {  return useRender({    defaultTagName: 'button',    props: mergeProps<'button'>(      {        className: cn(          'absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden group-data-[collapsible=icon]:w-4 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0',          className        ),      },      props    ),    render,    state: {      slot: 'sidebar-group-action',      sidebar: 'group-action',    },  });}function McSidebarGroupContent({ 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 McSidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {  return (    <ul      data-slot="sidebar-menu"      data-sidebar="menu"      className={cn('m-0 flex w-full min-w-0 list-none flex-col gap-0 p-0', className)}      {...props}    />  );}function McSidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {  return (    <li      data-slot="sidebar-menu-item"      data-sidebar="menu-item"      className={cn('list-none group/menu-item relative', className)}      {...props}    />  );}type McSidebarCollapsibleProps = Omit<React.ComponentProps<'li'>, 'children'> & {  label: React.ReactNode;  icon?: React.ComponentType<{ className?: string }>;  tooltip?: string;  defaultOpen?: boolean;  open?: boolean;  onOpenChange?: (open: boolean) => void;  triggerClassName?: string;  contentClassName?: string;  chevronClassName?: string;  children?: React.ReactNode;};function McSidebarCollapsible({  label,  icon: Icon,  tooltip,  defaultOpen = false,  open: openProp,  onOpenChange,  className,  triggerClassName,  contentClassName,  chevronClassName,  children,  ...props}: McSidebarCollapsibleProps) {  const hasChildren = React.Children.count(children) > 0;  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);  const isOpen = openProp ?? uncontrolledOpen;  const setOpen = React.useCallback(    (value: boolean) => {      if (openProp === undefined) {        setUncontrolledOpen(value);      }      onOpenChange?.(value);    },    [openProp, onOpenChange]  );  const handleToggle = React.useCallback(() => {    if (!hasChildren) {      return;    }    setOpen(!isOpen);  }, [hasChildren, isOpen, setOpen]);  return (    <McSidebarMenuItem className={cn('h-auto', className)} {...props}>      <McSidebarMenuButton        tooltip={tooltip}        onClick={handleToggle}        aria-expanded={hasChildren ? isOpen : undefined}        className={cn('justify-between', triggerClassName)}      >        <span className="flex items-center gap-2 group-data-[collapsible=icon]:gap-0">          {Icon ? <Icon /> : null}          <span className="group-data-[collapsible=icon]:hidden">{label}</span>        </span>        {hasChildren ? (          <ChevronDownIcon            className={cn(              'h-4 w-4 shrink-0 transition-transform group-data-[collapsible=icon]:hidden',              isOpen && 'rotate-180',              chevronClassName            )}          />        ) : null}      </McSidebarMenuButton>      {hasChildren && isOpen ? (        <McSidebarMenuSub className={contentClassName}>{children}</McSidebarMenuSub>      ) : null}    </McSidebarMenuItem>  );}const sidebarMenuButtonVariants = cva(  'peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:mx-auto group-data-[collapsible=icon]:p-0! group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:gap-0 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 group-data-[collapsible=icon]:[&_svg]:size-4 [&>span:last-child]:truncate group-data-[collapsible=icon]:[&>span>span]:hidden group-data-[collapsible=icon]:[&>svg+span]:hidden group-data-[collapsible=icon]:[&>span]:w-full group-data-[collapsible=icon]:[&>span]:justify-center group-data-[collapsible=icon]:[&>span]:gap-0',  {    variants: {      variant: {        default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',        outline:          'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',      },      size: {        default: 'h-8 text-sm',        sm: 'h-7 text-xs',        lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!',      },    },    defaultVariants: {      variant: 'default',      size: 'default',    },  });function McSidebarMenuButton({  render,  isActive = false,  variant = 'default',  size = 'default',  tooltip,  className,  ...props}: useRender.ComponentProps<'button'> &  React.ComponentProps<'button'> & {    isActive?: boolean;    tooltip?: string | React.HTMLAttributes<HTMLButtonElement>;  } & VariantProps<typeof sidebarMenuButtonVariants>) {  const { state } = useSidebar();  const tooltipLabel =    typeof tooltip === 'string'      ? tooltip      : typeof tooltip?.['aria-label'] === 'string'        ? tooltip['aria-label']        : undefined;  const comp = useRender({    defaultTagName: 'button',    props: mergeProps<'button'>(      {        type: 'button',        className: cn(sidebarMenuButtonVariants({ variant, size }), className),        title: state === 'collapsed' ? tooltipLabel : undefined,      },      props    ),    render,    state: {      slot: 'sidebar-menu-button',      sidebar: 'menu-button',      size,      active: isActive,    },  });  if (!tooltip) {    return comp;  }  return comp;}function McSidebarMenuAction({  className,  render,  showOnHover = false,  ...props}: useRender.ComponentProps<'button'> &  React.ComponentProps<'button'> & {    showOnHover?: boolean;  }) {  return useRender({    defaultTagName: 'button',    props: mergeProps<'button'>(      {        className: cn(          'absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden group-data-[collapsible=icon]:w-4 peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0',          showOnHover &&            'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0',          className        ),      },      props    ),    render,    state: {      slot: 'sidebar-menu-action',      sidebar: 'menu-action',    },  });}function McSidebarMenuBadge({ className, ...props }: React.ComponentProps<'div'>) {  return (    <div      data-slot="mc-sidebar-menu-badge"      data-sidebar="menu-badge"      className={cn(        'pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium text-sidebar-foreground tabular-nums select-none group-data-[collapsible=icon]:hidden group-data-[collapsible=icon]:h-4 group-data-[collapsible=icon]:w-4 group-data-[collapsible=icon]:min-w-4 peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 peer-data-active/menu-button:text-sidebar-accent-foreground',        className      )}      {...props}    />  );}function McSidebarMenuSkeleton({  className,  showIcon = false,  ...props}: React.ComponentProps<'div'> & {  showIcon?: boolean;}) {  const [width] = React.useState(() => {    return `${Math.floor(Math.random() * 40) + 50}%`;  });  return (    <div      data-slot="mc-sidebar-menu-skeleton"      data-sidebar="menu-skeleton"      className={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}      {...props}    >      {showIcon && (        <div          className="size-4 rounded-md bg-muted animate-pulse"          data-sidebar="menu-skeleton-icon"        />      )}      <div        className="h-4 max-w-(--skeleton-width) flex-1 rounded-md bg-muted animate-pulse"        data-sidebar="menu-skeleton-text"        style={          {            '--skeleton-width': width,          } as React.CSSProperties        }      />    </div>  );}function McSidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {  return (    <ul      data-slot="sidebar-menu-sub"      data-sidebar="menu-sub"      className={cn(        'mx-3.5 m-0 flex min-w-0 list-none translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden group-data-[collapsible=icon]:mx-1 group-data-[collapsible=icon]:px-1',        className      )}      {...props}    />  );}function McSidebarMenuSubItem({ className, ...props }: React.ComponentProps<'li'>) {  return (    <li      data-slot="sidebar-menu-sub-item"      data-sidebar="menu-sub-item"      className={cn('list-none group/menu-sub-item relative', className)}      {...props}    />  );}function McSidebarMenuSubButton({  render,  size = 'md',  isActive = false,  className,  ...props}: useRender.ComponentProps<'a'> &  React.ComponentProps<'a'> & {    size?: 'sm' | 'md';    isActive?: boolean;  }) {  return useRender({    defaultTagName: 'a',    props: mergeProps<'a'>(      {        className: cn(          'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden group-data-[collapsible=icon]:px-1 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground',          className        ),      },      props    ),    render,    state: {      slot: 'sidebar-menu-sub-button',      sidebar: 'menu-sub-button',      size,      active: isActive,    },  });}export {  McSidebar,  McSidebarContent,  McSidebarFooter,  McSidebarGroup,  McSidebarGroupAction,  McSidebarGroupContent,  McSidebarGroupLabel,  McSidebarHeader,  McSidebarInset,  McSidebarMenu,  McSidebarMenuAction,  McSidebarMenuBadge,  McSidebarMenuButton,  McSidebarCollapsible,  McSidebarMenuItem,  McSidebarMenuSkeleton,  McSidebarMenuSub,  McSidebarMenuSubButton,  McSidebarMenuSubItem,  SidebarProvider,  McSidebarRail,  McSidebarSeparator,  McSidebarTrigger,  useSidebar,};
Adjust import paths

Ensure your @/lib/utils (or equivalent classname helper) and component import aliases are correctly configured.

Usage

import {
  SidebarProvider,
  McSidebar,
  McSidebarContent,
  McSidebarInset,
} from '@/components/ui/mc-sidebar';

export default function Example() {
  return (
    <SidebarProvider>
      <McSidebar>
        <McSidebarContent>{/* sidebar items */}</McSidebarContent>
      </McSidebar>
      <McSidebarInset>{/* page content */}</McSidebarInset>
    </SidebarProvider>
  );
}

Examples

Basic Layout

<SidebarProvider defaultOpen>
  <McSidebar>
    <McSidebarHeader>{/* header items */}</McSidebarHeader>
    <McSidebarContent>{/* menu groups */}</McSidebarContent>
    <McSidebarFooter>{/* footer items */}</McSidebarFooter>
  </McSidebar>
  <McSidebarInset>{/* main content */}</McSidebarInset>
</SidebarProvider>

Variants

<McSidebar variant="sidebar" />
<McSidebar variant="floating" />
<McSidebar variant="inset" />

Collapsible Modes

<McSidebar collapsible="icon" />
<McSidebar collapsible="offcanvas" />
<McSidebar collapsible="none" />

Controlled Open State

Manage sidebar visibility programmatically with open and onOpenChange on SidebarProvider.

import { useState } from 'react';
import { SidebarProvider, McSidebar } from '@/components/ui/mc-sidebar';

export default function Example() {
  const [open, setOpen] = useState(true);

  return (
    <SidebarProvider open={open} onOpenChange={setOpen}>
      <McSidebar />
    </SidebarProvider>
  );
}

Nested Navigation with Collapsible Items

<McSidebarMenu>
  <McSidebarCollapsible label="Projects" tooltip="Projects">
    <McSidebarMenuSubItem>
      <McSidebarMenuSubButton href="#">Backlog</McSidebarMenuSubButton>
    </McSidebarMenuSubItem>
    <McSidebarMenuSubItem>
      <McSidebarMenuSubButton href="#">Sprints</McSidebarMenuSubButton>
    </McSidebarMenuSubItem>
  </McSidebarCollapsible>
</McSidebarMenu>

The McSidebarMenuButton supports different visual variants for various use cases.

<McSidebarMenuButton variant="default">Default Style</McSidebarMenuButton>

<McSidebarMenuButton variant="outline" className="mt-2">
  Outline Style
</McSidebarMenuButton>

Choose the appropriate size for your menu items.

<McSidebarMenuButton size="sm">Small</McSidebarMenuButton>
<McSidebarMenuButton size="default">Default</McSidebarMenuButton>
<McSidebarMenuButton size="lg" className="mt-2">Large</McSidebarMenuButton>

Active State

Use isActive prop to highlight the currently active menu item.

<McSidebarMenuButton isActive>Active Item</McSidebarMenuButton>

<McSidebarMenuButton>Regular Item</McSidebarMenuButton>

A button component to toggle the sidebar open/closed state.

<McSidebarTrigger />

A thin rail component that provides an alternative way to toggle the sidebar.

<McSidebarRail />

Organize menu items using groups with labels.

<McSidebarGroup>
  <McSidebarGroupLabel>Navigation</McSidebarGroupLabel>
  <McSidebarMenu>
    <McSidebarMenuItem>
      <McSidebarMenuButton>Dashboard</McSidebarMenuButton>
    </McSidebarMenuItem>
    <McSidebarMenuItem>
      <McSidebarMenuButton>Settings</McSidebarMenuButton>
    </McSidebarMenuItem>
  </McSidebarMenu>
</McSidebarGroup>

Display badges or counts on menu items.

<McSidebarMenuItem>
  <McSidebarMenuButton>Notifications</McSidebarMenuButton>
  <McSidebarMenuBadge>5</McSidebarMenuBadge>
</McSidebarMenuItem>

Show loading state while content is being fetched.

<McSidebarMenuSkeleton showIcon />
<McSidebarMenuSkeleton />

API Reference

SidebarProvider Props

PropTypeDefaultDescription
defaultOpenbooleantrueInitial open state for uncontrolled usage
openbooleanundefinedControlled open state
onOpenChange(open: boolean) => voidundefinedCallback fired when open state changes
classNamestringundefinedAdditional classes for sidebar wrapper
childrenReact.ReactNodeundefinedSidebar and inset content

Also supports standard div props.

McSidebar Props

PropTypeDefaultDescription
side"left" | "right""left"Sidebar side placement
variant"sidebar" | "floating" | "inset""sidebar"Visual style variant
collapsible"offcanvas" | "icon" | "none""icon"Collapse behavior mode
classNamestringundefinedAdditional classes for sidebar container
childrenReact.ReactNodeundefinedSidebar internal content

Also supports standard div props.

McSidebarCollapsible Props

PropTypeDefaultDescription
labelReact.ReactNoderequiredLabel content displayed in the trigger
iconReact.ComponentType<{ className?: string }>undefinedOptional icon displayed before the label
tooltipstringundefinedTooltip label when sidebar is collapsed
defaultOpenbooleanfalseInitial open state for uncontrolled usage
openbooleanundefinedControlled open state
onOpenChange(open: boolean) => voidundefinedCallback fired when collapsible state changes
triggerClassNamestringundefinedExtra classes for the trigger button
contentClassNamestringundefinedExtra classes for nested content container
chevronClassNamestringundefinedExtra classes for chevron icon
childrenReact.ReactNodeundefinedNested menu items rendered when expanded

Also supports standard li props (except children is managed by the component API).

McSidebarMenuButton Props

PropTypeDefaultDescription
isActivebooleanfalseWhether the button is currently active
variant"default" | "outline""default"Visual style variant
size"default" | "sm" | "lg""default"Size of the menu button
tooltipstring | React.HTMLAttributes<HTMLButtonElement>undefinedTooltip to display when sidebar is collapsed
classNamestringundefinedAdditional CSS classes
renderReactElement | ((props: HTMLProps) => ReactElement)undefinedPolymorphic render prop

Also supports standard button props.

McSidebarMenuAction Props

PropTypeDefaultDescription
showOnHoverbooleanfalseWhether to show the action button on hover
classNamestringundefinedAdditional CSS classes
renderReactElement | ((props: HTMLProps) => ReactElement)undefinedPolymorphic render prop

Also supports standard button props.

McSidebarMenuSubButton Props

PropTypeDefaultDescription
size"sm" | "md""md"Size of the sub-button
isActivebooleanfalseWhether the sub-button is active
classNamestringundefinedAdditional CSS classes
renderReactElement | ((props: HTMLProps) => ReactElement)undefinedPolymorphic render prop

Also supports standard a props.

McSidebarGroupLabel Props

PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes
renderReactElement | ((props: HTMLProps) => ReactElement)undefinedPolymorphic render prop

Also supports standard div props.

McSidebarGroupAction Props

PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes
renderReactElement | ((props: HTMLProps) => ReactElement)undefinedPolymorphic render prop

Also supports standard button props.

McSidebarTrigger Props

PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes

Also supports props from McButton.

McSidebarRail Props

PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes

Also supports standard button props.

McSidebarHeader Props

PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes

Also supports standard div props.

McSidebarFooter Props

PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes

Also supports standard div props.

McSidebarContent Props

PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes

Also supports standard div props.

McSidebarGroup Props

PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes

Also supports standard div props.

McSidebarGroupContent Props

PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes

Also supports standard div props.

McSidebarMenu Props

PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes

Also supports standard ul props.

McSidebarMenuItem Props

PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes

Also supports standard li props.

McSidebarMenuSub Props

PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes

Also supports standard ul props.

McSidebarMenuSubItem Props

PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes

Also supports standard li props.

McSidebarSeparator Props

PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes

Also supports standard div props.

McSidebarMenuBadge Props

PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes

Also supports standard div props.

McSidebarMenuSkeleton Props

PropTypeDefaultDescription
showIconbooleanfalseWhether to show a loading icon
classNamestringundefinedAdditional CSS classes

Also supports standard div props.

McSidebarInset Props

PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes

Also supports standard main props.

useSidebar Hook

A hook to access the sidebar context state and controls.

Return ValueTypeDescription
state"expanded" | "collapsed"Current sidebar state
openbooleanWhether the sidebar is open
setOpen(open: boolean) => voidFunction to set open state
toggleSidebar() => voidFunction to toggle sidebar

On this page