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
'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:
This also installs mc-button and mc-tooltip.
Manual Setup
If you prefer manual control:
mc-button and mc-tooltip'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,};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>Sidebar Menu Button Variants
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>Sidebar Menu Button Sizes
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>Sidebar Trigger
A button component to toggle the sidebar open/closed state.
<McSidebarTrigger />Sidebar Rail
A thin rail component that provides an alternative way to toggle the sidebar.
<McSidebarRail />Sidebar with Groups
Organize menu items using groups with labels.
<McSidebarGroup>
<McSidebarGroupLabel>Navigation</McSidebarGroupLabel>
<McSidebarMenu>
<McSidebarMenuItem>
<McSidebarMenuButton>Dashboard</McSidebarMenuButton>
</McSidebarMenuItem>
<McSidebarMenuItem>
<McSidebarMenuButton>Settings</McSidebarMenuButton>
</McSidebarMenuItem>
</McSidebarMenu>
</McSidebarGroup>Sidebar with Badges
Display badges or counts on menu items.
<McSidebarMenuItem>
<McSidebarMenuButton>Notifications</McSidebarMenuButton>
<McSidebarMenuBadge>5</McSidebarMenuBadge>
</McSidebarMenuItem>Sidebar with Skeleton Loading
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-expandedandaria-controls. - Nested menus that are closed or collapsed are removed from the tab order.
McSidebarTriggerandMcSidebarRailhave the label "Toggle Sidebar".- Below the
mdbreakpoint the sidebar is not rendered. Provide your own mobile navigation.
API Reference
McSidebarProvider Props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOpen | boolean | true | Initial open state for uncontrolled usage |
open | boolean | undefined | Controlled open state |
onOpenChange | (open: boolean) => void | undefined | Callback fired when open state changes |
className | string | undefined | Additional classes for sidebar wrapper |
children | React.ReactNode | undefined | Sidebar and inset content |
Also supports standard div props. SidebarProvider is a deprecated alias of McSidebarProvider.
McSidebar Props
| Prop | Type | Default | Description |
|---|---|---|---|
side | "left" | "right" | "left" | Sidebar side placement |
variant | "sidebar" | "floating" | "inset" | "sidebar" | Visual style variant |
collapsible | "offcanvas" | "icon" | "none" | "icon" | Collapse behavior mode |
className | string | undefined | Additional classes for sidebar container |
children | React.ReactNode | undefined | Sidebar internal content |
Also supports standard div props.
McSidebarCollapsible Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | React.ReactNode | required | Label content displayed in the trigger |
icon | React.ComponentType<{ className?: string }> | undefined | Optional icon displayed before the label |
tooltip | string | undefined | Tooltip shown next to the icon when collapsed |
defaultOpen | boolean | false | Initial open state for uncontrolled usage |
open | boolean | undefined | Controlled open state |
onOpenChange | (open: boolean) => void | undefined | Callback fired when collapsible state changes |
triggerClassName | string | undefined | Extra classes for the trigger button |
contentClassName | string | undefined | Extra classes for nested content container |
chevronClassName | string | undefined | Extra classes for chevron icon |
children | React.ReactNode | undefined | Nested menu items rendered when expanded |
Also supports standard li props (except children is managed by the component API).
McSidebarMenuButton Props
| Prop | Type | Default | Description |
|---|---|---|---|
isActive | boolean | false | Whether the button is currently active |
variant | "default" | "outline" | "default" | Visual style variant |
size | "default" | "sm" | "lg" | "default" | Size of the menu button |
tooltip | string | React.HTMLAttributes<HTMLButtonElement> | undefined | Tooltip to display when sidebar is collapsed |
className | string | undefined | Additional CSS classes |
render | ReactElement | ((props: HTMLProps) => ReactElement) | undefined | Polymorphic render prop |
Also supports standard button props.
McSidebarMenuAction Props
| Prop | Type | Default | Description |
|---|---|---|---|
showOnHover | boolean | false | Whether to show the action button on hover |
className | string | undefined | Additional CSS classes |
render | ReactElement | ((props: HTMLProps) => ReactElement) | undefined | Polymorphic render prop |
Also supports standard button props.
McSidebarMenuSubButton Props
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "md" | Size of the sub-button |
isActive | boolean | false | Whether the sub-button is active |
className | string | undefined | Additional CSS classes |
render | ReactElement | ((props: HTMLProps) => ReactElement) | undefined | Polymorphic render prop |
Also supports standard a props.
McSidebarGroupLabel Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
render | ReactElement | ((props: HTMLProps) => ReactElement) | undefined | Polymorphic render prop |
Also supports standard div props.
McSidebarGroupAction Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
render | ReactElement | ((props: HTMLProps) => ReactElement) | undefined | Polymorphic render prop |
Also supports standard button props.
McSidebarTrigger Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
Also supports props from McButton.
McSidebarRail Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
Also supports standard button props.
McSidebarHeader Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
Also supports standard div props.
McSidebarFooter Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
Also supports standard div props.
McSidebarContent Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
Also supports standard div props.
McSidebarGroup Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
Also supports standard div props.
McSidebarGroupContent Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
Also supports standard div props.
McSidebarMenu Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
Also supports standard ul props.
McSidebarMenuItem Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
Also supports standard li props.
McSidebarMenuSub Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
Also supports standard ul props.
McSidebarMenuSubItem Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
Also supports standard li props.
McSidebarSeparator Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
Also supports standard div props.
McSidebarMenuBadge Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
Also supports standard div props.
McSidebarMenuSkeleton Props
| Prop | Type | Default | Description |
|---|---|---|---|
showIcon | boolean | false | Whether to show a loading icon |
className | string | undefined | Additional CSS classes |
Also supports standard div props.
McSidebarFade Props
Fades its content out when the sidebar collapses.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
Also supports standard div props.
McSidebarInset Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
Also supports standard main props.
useMcSidebar Hook
A hook to access the sidebar context state and controls.
| Return Value | Type | Description |
|---|---|---|
state | "expanded" | "collapsed" | Current sidebar state |
open | boolean | Whether the sidebar is open |
setOpen | (open: boolean) => void | Function to set open state |
toggleSidebar | () => void | Function to toggle sidebar |
useSidebar is a deprecated alias of useMcSidebar.