42

MC Sidebar

A sidebar navigation component.

Overview

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

Opening and closing is animated as one 300ms motion: the width, the labels, the group titles and the nested menus all move together, and it is skipped for users who prefer reduced motion.

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

Preview

Loading...
'use client';import {  Bot,  BookOpen,  ChevronsUpDown,  Frame,  Map,  MoreHorizontal,  PieChart,  Settings2,  SquareTerminal,} from 'lucide-react';import {  McSidebar,  McSidebarCollapsible,  McSidebarContent,  McSidebarFade,  McSidebarFooter,  McSidebarGroup,  McSidebarGroupContent,  McSidebarGroupLabel,  McSidebarHeader,  McSidebarInset,  McSidebarMenu,  McSidebarMenuButton,  McSidebarMenuItem,  McSidebarMenuSubButton,  McSidebarMenuSubItem,  McSidebarProvider,  McSidebarRail,  McSidebarTrigger,} from '@/components/ui/mc-sidebar';const platformItems = [  { label: 'Playground', icon: SquareTerminal, items: ['History', 'Starred', 'Settings'] },  { label: 'Models', icon: Bot, items: ['Genesis', 'Explorer', 'Quantum'] },  { label: 'Documentation', icon: BookOpen, items: ['Introduction', 'Get Started', 'Tutorials'] },  { label: 'Settings', icon: Settings2, items: ['General', 'Team', 'Billing'] },];const projectItems = [  { label: 'Design Engineering', icon: Frame },  { label: 'Sales & Marketing', icon: PieChart },  { label: 'Travel', icon: Map },  { label: 'More', icon: MoreHorizontal },];export function McSidebarDemoLayout({  side = 'left',  variant = 'sidebar',  collapsible = 'icon',}: {  side?: 'left' | 'right';  variant?: 'sidebar' | 'floating' | 'inset';  collapsible?: 'offcanvas' | 'icon' | 'none';}) {  return (    <>      <McSidebar        side={side}        variant={variant}        collapsible={collapsible}        className="h-[calc(100%-2.5rem)]"      >        <McSidebarHeader>          <div className="flex items-center gap-2">            <div className="flex size-[35px] shrink-0 items-center justify-center rounded-lg bg-sidebar-primary text-sm font-semibold text-sidebar-primary-foreground">              MC            </div>            <McSidebarFade className="flex flex-1 items-center gap-2">              <div className="min-w-0 flex-1">                <p className="text-sm leading-5 font-semibold">Micro Club</p>                <p className="text-xs leading-4">Scientific club</p>              </div>              <ChevronsUpDown className="size-4 shrink-0" />            </McSidebarFade>          </div>        </McSidebarHeader>        <McSidebarContent>          <McSidebarGroup>            <McSidebarGroupLabel>Platform</McSidebarGroupLabel>            <McSidebarGroupContent>              <McSidebarMenu>                {platformItems.map((item, index) => (                  <McSidebarCollapsible                    key={item.label}                    label={item.label}                    icon={item.icon}                    tooltip={item.label}                    defaultOpen={index === 0}                  >                    {item.items.map((subItem) => (                      <McSidebarMenuSubItem key={subItem}>                        <McSidebarMenuSubButton href="#">{subItem}</McSidebarMenuSubButton>                      </McSidebarMenuSubItem>                    ))}                  </McSidebarCollapsible>                ))}              </McSidebarMenu>            </McSidebarGroupContent>          </McSidebarGroup>          <McSidebarGroup>            <McSidebarGroupLabel>Projects</McSidebarGroupLabel>            <McSidebarGroupContent>              <McSidebarMenu>                {projectItems.map((item) => (                  <McSidebarMenuItem key={item.label}>                    <McSidebarMenuButton tooltip={item.label}>                      <item.icon />                      <span>{item.label}</span>                    </McSidebarMenuButton>                  </McSidebarMenuItem>                ))}              </McSidebarMenu>            </McSidebarGroupContent>          </McSidebarGroup>        </McSidebarContent>        <McSidebarFooter>          <div className="flex items-center gap-2">            <div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-accent text-xs font-medium text-sidebar-accent-foreground">              MC            </div>            <McSidebarFade className="flex flex-1 items-center gap-2">              <div className="min-w-0 flex-1">                <p className="truncate text-sm leading-5 font-semibold">MC</p>                <p className="truncate text-xs leading-4">micro@gmail.com</p>              </div>              <ChevronsUpDown className="size-4 shrink-0" />            </McSidebarFade>          </div>        </McSidebarFooter>        {collapsible !== 'none' ? <McSidebarRail /> : null}      </McSidebar>      <McSidebarInset>        <header className="flex h-12 shrink-0 items-center gap-2 border-b px-3">          {collapsible !== 'none' ? <McSidebarTrigger /> : null}          <p className="text-sm font-medium">Preview</p>        </header>        <div className="p-4 text-sm text-muted-foreground">          Use the button or press Cmd/Ctrl + B to collapse the sidebar.        </div>      </McSidebarInset>    </>  );}export default function McSidebarDemo() {  return (    <McSidebarProvider className="relative h-[28rem] min-h-0 overflow-hidden rounded-xl border [transform:translateZ(0)]">      <McSidebarDemoLayout />    </McSidebarProvider>  );}

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

This also installs mc-button and mc-tooltip.

Manual Setup

If you prefer manual control:

Install the core dependencies
npm install @base-ui/react lucide-react class-variance-authority
pnpm add @base-ui/react lucide-react class-variance-authority
yarn add @base-ui/react lucide-react class-variance-authority
bun add @base-ui/react lucide-react class-variance-authority
Add mc-button and mc-tooltip
Copy the source code
'use client';import * as React from 'react';import { Collapsible as CollapsiblePrimitive } from '@base-ui/react/collapsible';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 { ChevronRightIcon, PanelLeftIcon } from 'lucide-react';import { cn } from '@/lib/utils';import { McButton } from '@/components/ui/mc-button';import { McTooltip, McTooltipContent, McTooltipTrigger } from '@/components/ui/mc-tooltip';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';const SIDEBAR_TRANSITION =  'duration-300 ease-in-out motion-reduce:transition-none group-data-[ready=false]:transition-none';type SidebarContextProps = {  state: 'expanded' | 'collapsed';  open: boolean;  ready: boolean;  setOpen: (open: boolean) => void;  toggleSidebar: () => void;};const SidebarContext = React.createContext<SidebarContextProps | null>(null);function useMcSidebar() {  const context = React.useContext(SidebarContext);  if (!context) {    throw new Error('useMcSidebar must be used within a McSidebarProvider.');  }  return context;}function McSidebarProvider({  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(defaultOpen);  const [ready, setReady] = React.useState(false);  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);      }      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;    },    [setOpenProp, open]  );  React.useEffect(() => {    if (openProp === undefined) {      const cookie = document.cookie        .split('; ')        .find((entry) => entry.startsWith(`${SIDEBAR_COOKIE_NAME}=`));      if (cookie) {        _setOpen(cookie.split('=')[1] === 'true');      }    }    let second = 0;    const first = requestAnimationFrame(() => {      second = requestAnimationFrame(() => setReady(true));    });    return () => {      cancelAnimationFrame(first);      cancelAnimationFrame(second);    };  }, [openProp]);  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,      ready,      setOpen,      toggleSidebar,    }),    [state, open, ready, 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, ready } = useMcSidebar();  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-ready={ready ? 'true' : 'false'}      data-slot="sidebar"    >      <div        data-slot="sidebar-gap"        className={cn(          'relative w-0 bg-transparent transition-[width]',          SIDEBAR_TRANSITION,          '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] 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)',          SIDEBAR_TRANSITION,          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 } = useMcSidebar();  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 } = useMcSidebar();  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-colors duration-200 ease-in-out 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', 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', className)}      {...props}    />  );}function McSidebarFade({ className, ...props }: React.ComponentProps<'div'>) {  return (    <div      data-slot="sidebar-fade"      className={cn(        'min-w-0 whitespace-nowrap transition-opacity group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:opacity-0',        SIDEBAR_TRANSITION,        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', 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', 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 whitespace-nowrap text-sidebar-foreground opacity-70 ring-sidebar-ring outline-hidden transition-[height,opacity] group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:h-0 group-data-[collapsible=icon]:overflow-hidden group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',          SIDEBAR_TRANSITION,          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,opacity] group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:opacity-0 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',          SIDEBAR_TRANSITION,          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-1 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 contentId = React.useId();  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]);  const { state } = useMcSidebar();  return (    <McSidebarMenuItem className={cn('h-auto', className)} {...props}>      <CollapsiblePrimitive.Root open={isOpen && state === 'expanded'}>        <McSidebarMenuButton          tooltip={tooltip}          onClick={handleToggle}          aria-expanded={hasChildren ? isOpen : undefined}          aria-controls={hasChildren ? contentId : undefined}          className={cn('justify-between', triggerClassName)}        >          <span className="flex items-center gap-2">            {Icon ? <Icon /> : null}            <span>{label}</span>          </span>          {hasChildren ? (            <ChevronRightIcon              className={cn(                'size-4 shrink-0 transition-[transform,opacity] duration-300 ease-in-out group-data-[collapsible=icon]:opacity-0',                isOpen && 'rotate-90',                chevronClassName              )}            />          ) : null}        </McSidebarMenuButton>        {hasChildren ? (          <CollapsiblePrimitive.Panel            id={contentId}            className="h-(--collapsible-panel-height) overflow-hidden transition-[height,opacity] duration-300 ease-in-out data-ending-style:h-0 data-ending-style:opacity-0 data-starting-style:h-0 data-starting-style:opacity-0 motion-reduce:transition-none"          >            <McSidebarMenuSub className={contentClassName}>{children}</McSidebarMenuSub>          </CollapsiblePrimitive.Panel>        ) : null}      </CollapsiblePrimitive.Root>    </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 whitespace-nowrap ring-sidebar-ring outline-hidden transition-[padding,background-color,color] group-has-data-[sidebar=menu-action]/menu-item:pr-8 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 [&>span:last-child]:truncate [&>svg+span]:transition-opacity [&>span>span]:transition-opacity [&>svg+span]:duration-300 [&>span>span]:duration-300 group-data-[collapsible=icon]:[&>span>span]:opacity-0 group-data-[collapsible=icon]:[&>svg+span]:opacity-0',  {    variants: {      variant: {        default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',        outline:          'bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]',      },      size: {        default: 'h-8 text-sm',        sm: 'h-7 text-xs',        lg: 'h-12 text-sm',      },    },    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 } = useMcSidebar();  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 }), SIDEBAR_TRANSITION, className),      },      props    ),    render,    state: {      slot: 'sidebar-menu-button',      sidebar: 'menu-button',      size,      active: isActive,    },  });  if (!tooltipLabel) {    return comp;  }  return (    <McTooltip disabled={state !== 'collapsed'}>      <McTooltipTrigger render={comp} />      <McTooltipContent side="right" sideOffset={12}>        {tooltipLabel}      </McTooltipContent>    </McTooltip>  );}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,opacity] group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:opacity-0 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',          SIDEBAR_TRANSITION,          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 transition-opacity select-none group-data-[collapsible=icon]:opacity-0 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',        SIDEBAR_TRANSITION,        className      )}      {...props}    />  );}function McSidebarMenuSkeleton({  className,  showIcon = false,  ...props}: React.ComponentProps<'div'> & {  showIcon?: boolean;}) {  const id = React.useId();  const width = `${    50 + ([...id].reduce((hash, char) => (hash * 31 + char.charCodeAt(0)) % 997, 7) % 40)  }%`;  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 flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 transition-opacity duration-300 ease-in-out group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:opacity-0',        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 items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:pointer-events-none 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,    },  });}/** @deprecated Use `McSidebarProvider`. */const SidebarProvider = McSidebarProvider;/** @deprecated Use `useMcSidebar`. */const useSidebar = useMcSidebar;export {  McSidebar,  McSidebarContent,  McSidebarFade,  McSidebarFooter,  McSidebarGroup,  McSidebarGroupAction,  McSidebarGroupContent,  McSidebarGroupLabel,  McSidebarHeader,  McSidebarInset,  McSidebarMenu,  McSidebarMenuAction,  McSidebarMenuBadge,  McSidebarMenuButton,  McSidebarCollapsible,  McSidebarMenuItem,  McSidebarMenuSkeleton,  McSidebarMenuSub,  McSidebarMenuSubButton,  McSidebarMenuSubItem,  McSidebarProvider,  SidebarProvider,  McSidebarRail,  McSidebarSeparator,  McSidebarTrigger,  useMcSidebar,  useSidebar,};
Adjust import paths

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

Usage

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

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

Examples

Basic Layout

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

Variants

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

Collapsible Modes

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

Hiding content when collapsed

Do not hide content with hidden when the sidebar collapses, because it disappears before the width finishes animating. Wrap it in McSidebarFade to fade it out instead. Labels inside McSidebarMenuButton and McSidebarGroupLabel already fade on their own.

<McSidebarHeader>
  <div className="flex items-center gap-2">
    <Logo className="size-9 shrink-0" />
    <McSidebarFade>
      <p className="text-sm font-semibold">Micro Club</p>
      <p className="text-xs">Scientific club</p>
    </McSidebarFade>
  </div>
</McSidebarHeader>

Persisting the open state

The open state is saved in the sidebar_state cookie each time the user toggles it, and read again after the page mounts. Pass open and onOpenChange if you want to store it somewhere else. Use Cmd/Ctrl + B to toggle the sidebar from the keyboard.

Embedding in a container

The sidebar is fixed by default. To place it inside a card or a preview, give the wrapper position: relative and a transform, and set the height on McSidebar.

<McSidebarProvider className="relative h-[28rem] min-h-0 overflow-hidden [transform:translateZ(0)]">
  <McSidebar className="h-[calc(100%-2.5rem)]">{/* ... */}</McSidebar>
  <McSidebarInset>{/* ... */}</McSidebarInset>
</McSidebarProvider>

Controlled Open State

Manage sidebar visibility programmatically with open and onOpenChange on McSidebarProvider.

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

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

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

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 />

Accessibility

  • Collapsed menu buttons keep their text in the page, so screen readers still read the label. A tooltip shows the same label on hover and focus.
  • A button with a nested menu sets aria-expanded and aria-controls.
  • Nested menus that are closed or collapsed are removed from the tab order.
  • McSidebarTrigger and McSidebarRail have the label "Toggle Sidebar".
  • Below the md breakpoint the sidebar is not rendered. Provide your own mobile navigation.

API Reference

McSidebarProvider 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. SidebarProvider is a deprecated alias of McSidebarProvider.

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 shown next to the icon when 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.

McSidebarFade Props

Fades its content out when the sidebar collapses.

PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes

Also supports standard div props.

McSidebarInset Props

PropTypeDefaultDescription
classNamestringundefinedAdditional CSS classes

Also supports standard main props.

useMcSidebar 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

useSidebar is a deprecated alias of useMcSidebar.

On this page