A composable, themeable and customizable sidebar component.
A sidebar that collapses to icons.
Sidebars are one of the most complex components to build. They are central to any application and often contain a lot of moving parts.
I don't like building sidebars. So I built 30+ of them. All kinds of
configurations. Then I extracted the core components into sidebar.tsx.
We now have a solid foundation to build on top of. Composable. Themeable. Customizable.
Installation
npx shadcn@latest add @caprice/sidebarnpm install @caprice-ui/reactInstall the following dependencies:
npm install @base-ui/react lucide-reactAdd a cn helper
import { defineConfig, type VariantProps } from 'cva';import { twMerge } from 'tailwind-merge';export const { compose, cva, cx: cn,} = defineConfig({ hooks: { onComplete: (className: string) => twMerge(className), },});export type { VariantProps };Copy and paste the following code into your project.
'use client';import { mergeProps, useRender } from '@base-ui/react';import { PanelLeftIcon } from 'lucide-react';import * as React from 'react';import { Button } from '@/components/caprice-ui/button';import { Input as CapriceInput } from '@/components/caprice-ui/input';import { Separator as CapriceSeparator } from '@/components/caprice-ui/separator';import * as Sheet from '@/components/caprice-ui/sheet';import { Skeleton } from '@/components/caprice-ui/skeleton';import * as Tooltip from '@/components/caprice-ui/tooltip';import { useIsMobile } from '@/hooks/use-mobile';import { cn, cva, type VariantProps } from '@/lib/utils';const SIDEBAR_COOKIE_NAME = 'sidebar_state';const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;const SIDEBAR_WIDTH = '16rem';const SIDEBAR_WIDTH_MOBILE = '18rem';const SIDEBAR_WIDTH_ICON = '3rem';const SIDEBAR_KEYBOARD_SHORTCUT = 'b';type SidebarContextProps = { state: 'expanded' | 'collapsed'; open: boolean; setOpen: (open: boolean) => void; openMobile: boolean; setOpenMobile: (open: boolean) => void; isMobile: boolean; toggleSidebar: () => void;};const SidebarContext = React.createContext<SidebarContextProps | null>(null);/** * Hook to use the sidebar context. * * @throws {Error} If the hook is used outside of a Sidebar.Provider. */export function useSidebar() { const context = React.useContext(SidebarContext); if (!context) { throw new Error('useSidebar must be used within a Sidebar.Provider.'); } return context;}export namespace Provider { export type State = SidebarContextProps; export type Props = React.ComponentProps<'div'> & { /** * The default open state of the sidebar. * @default true */ defaultOpen?: boolean; /** * The open state of the sidebar. */ open?: boolean; /** * The function to call when the open state changes. */ onOpenChange?: (open: boolean) => void; };}/** * Provides the sidebar context to its children. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export function Provider({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props}: Provider.Props) { const isMobile = useIsMobile(); const [openMobile, setOpenMobile] = React.useState(false); // This is the internal state of the sidebar. // We use openProp and setOpenProp for control from outside the component. const [_open, _setOpen] = React.useState(defaultOpen); 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); } // This sets the cookie to keep the sidebar state. // biome-ignore lint/suspicious/noDocumentCookie: fine document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; }, [setOpenProp, open] ); // Helper to toggle the sidebar. // biome-ignore lint/correctness/useExhaustiveDependencies: fine const toggleSidebar = React.useCallback(() => { return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); }, [isMobile, setOpen, setOpenMobile]); // Adds a keyboard shortcut to toggle the sidebar. 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]); // We add a state so that we can do data-state="expanded" or "collapsed". // This makes it easier to style the sidebar with Tailwind classes. const state = open ? 'expanded' : 'collapsed'; // biome-ignore lint/correctness/useExhaustiveDependencies: fine const contextValue = React.useMemo<SidebarContextProps>( () => ({ state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, }), [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] ); return ( <SidebarContext.Provider value={contextValue}> <Tooltip.Provider delay={0}> <div className={cn( 'group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar', className )} data-slot="sidebar-wrapper" style={ { '--sidebar-width': SIDEBAR_WIDTH, '--sidebar-width-icon': SIDEBAR_WIDTH_ICON, ...style, } as React.CSSProperties } {...props} > {children} </div> </Tooltip.Provider> </SidebarContext.Provider> );}export namespace Root { export type Props = React.ComponentProps<'div'> & { /** * The side of the sidebar. * @default 'left' */ side?: 'left' | 'right'; /** * The variant of the sidebar. * @default 'sidebar' */ variant?: 'sidebar' | 'floating' | 'inset'; /** * The collapsible state of the sidebar. * @default 'offcanvas' */ collapsible?: 'offcanvas' | 'icon' | 'none'; };}export function Root({ side = 'left', variant = 'sidebar', collapsible = 'offcanvas', className, children, ...props}: Root.Props) { const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); if (collapsible === 'none') { return ( <div className={cn( 'flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground', className )} data-slot="sidebar" {...props} > {children} </div> ); } if (isMobile) { return ( <Sheet.Root onOpenChange={setOpenMobile} open={openMobile} {...props}> <Sheet.Popup className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden" data-mobile="true" data-sidebar="sidebar" data-slot="sidebar" side={side} style={ { '--sidebar-width': SIDEBAR_WIDTH_MOBILE, } as React.CSSProperties } > <Sheet.Header className="sr-only"> <Sheet.Title>Sidebar</Sheet.Title> <Sheet.Description>Displays the mobile sidebar.</Sheet.Description> </Sheet.Header> <div className="flex h-full w-full flex-col">{children}</div> </Sheet.Popup> </Sheet.Root> ); } return ( <div className="group peer hidden text-sidebar-foreground md:block" data-collapsible={state === 'collapsed' ? collapsible : ''} data-side={side} data-slot="sidebar" data-state={state} data-variant={variant} > {/* This is what handles the sidebar gap on desktop */} <div className={cn( 'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear', 'group-data-[collapsible=offcanvas]:w-0', 'group-data-[side=right]:rotate-180', variant === 'floating' || variant === 'inset' ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]' : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)' )} data-slot="sidebar-gap" /> <div className={cn( 'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex', side === 'left' ? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]' : 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]', // Adjust the padding for floating and inset variants. variant === 'floating' || variant === 'inset' ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]' : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l', className )} data-slot="sidebar-container" {...props} > <div className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm" data-sidebar="sidebar" data-slot="sidebar-inner" > {children} </div> </div> </div> );}/** * A button that toggles the sidebar. * Renders a `<button>` element. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace Trigger { export type Props = Button.Props;}export function Trigger({ className, onClick, ...props }: Trigger.Props) { const { toggleSidebar } = useSidebar(); return ( <Button className={cn('size-7', className)} data-sidebar="trigger" data-slot="sidebar-trigger" onClick={(event) => { onClick?.(event); toggleSidebar(); }} size="icon" variant="ghost" {...props} > <PanelLeftIcon /> <span className="sr-only">Toggle Sidebar</span> </Button> );}/** * A rail that can be used to toggle the sidebar. * Renders a `<button>` element. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace Rail { export type Props = useRender.ComponentProps<'button'>;}export function Rail({ className, render, ...props }: Rail.Props) { const { toggleSidebar } = useSidebar(); const defaultProps: useRender.ElementProps<'button'> & { 'data-slot'?: string; 'data-sidebar'?: string; } = { 'data-slot': 'sidebar-rail', 'data-sidebar': 'rail', 'aria-label': 'Toggle Sidebar', onClick: toggleSidebar, tabIndex: -1, title: 'Toggle Sidebar', className: cn( 'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex', 'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize', '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize', 'group-data-[collapsible=offcanvas]:translate-x-0 hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:after:left-full', '[[data-side=left][data-collapsible=offcanvas]_&]:-right-2', '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2', className ), }; return useRender({ defaultTagName: 'button', render, props: mergeProps<'button'>(defaultProps, props), });}/** * A main element that can be used to wrap the content of the sidebar. * Renders a `<main>` element. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace Inset { export type Props = useRender.ComponentProps<'main'>;}export function Inset({ className, render, ...props }: Inset.Props) { const defaultProps: useRender.ElementProps<'main'> & { 'data-slot'?: string } = { 'data-slot': 'sidebar-inset', className: cn( 'relative flex w-full flex-1 flex-col bg-background', 'md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm', className ), }; return useRender({ defaultTagName: 'main', render, props: mergeProps<'main'>(defaultProps, props), });}export namespace Input { export type Variants = CapriceInput.Variants; export type State = CapriceInput.State; export type Props = CapriceInput.Props; export type ChangeEventDetails = CapriceInput.ChangeEventDetails; export type ChangeEventReason = CapriceInput.ChangeEventReason;}export function Input({ className, ...props }: Input.Props) { return ( <CapriceInput className={cn('h-8 w-full bg-background shadow-none', className)} data-sidebar="input" data-slot="sidebar-input" {...props} /> );}/** * A Sticky header that can be used to display the title of the sidebar. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace Header { export type Props = useRender.ComponentProps<'div'>;}export function Header({ className, render, ...props }: Header.Props) { const defaultProps: useRender.ElementProps<'div'> & { 'data-slot'?: string; 'data-sidebar'?: string; } = { 'data-slot': 'sidebar-header', 'data-sidebar': 'header', className: cn('flex flex-col gap-2 p-2', className), }; return useRender({ defaultTagName: 'div', render, props: mergeProps<'div'>(defaultProps, props), });}/** * A sticky footer that can be used to display the footer of the sidebar. * Renders a `<div>` element. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace Footer { export type Props = useRender.ComponentProps<'div'>;}export function Footer({ className, render, ...props }: Footer.Props) { const defaultProps: useRender.ElementProps<'div'> & { 'data-slot'?: string; 'data-sidebar'?: string; } = { 'data-slot': 'sidebar-footer', 'data-sidebar': 'footer', className: cn('flex flex-col gap-2 p-2', className), }; return useRender({ defaultTagName: 'div', render, props: mergeProps<'div'>(defaultProps, props), });}export namespace Separator { export type State = CapriceSeparator.State; export type Props = CapriceSeparator.Props;}export function Separator({ className, ...props }: React.ComponentProps<typeof CapriceSeparator>) { return ( <CapriceSeparator className={cn('mx-2 w-auto bg-sidebar-border', className)} data-sidebar="separator" data-slot="sidebar-separator" {...props} /> );}/** * The Content component is used to wrap the content of the sidebar. * This is where you add your Group components. It is scrollable. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace Content { export type Props = useRender.ComponentProps<'div'>;}export function Content({ className, render, ...props }: Content.Props) { const defaultProps: useRender.ElementProps<'div'> & { 'data-slot'?: string; 'data-sidebar'?: string; } = { 'data-slot': 'sidebar-content', 'data-sidebar': 'content', className: cn( 'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden', className ), }; return useRender({ defaultTagName: 'div', render, props: mergeProps<'div'>(defaultProps, props), });}/** * The Group component is used to create a section within the sidebar. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace Group { export type Props = useRender.ComponentProps<'div'>;}export function Group({ className, render, ...props }: Group.Props) { const defaultProps: useRender.ElementProps<'div'> & { 'data-slot'?: string; 'data-sidebar'?: string; } = { 'data-slot': 'sidebar-group', 'data-sidebar': 'group', className: cn('relative flex w-full min-w-0 flex-col p-2', className), }; return useRender({ defaultTagName: 'div', render, props: mergeProps<'div'>(defaultProps, props), });}/** * A label that can be used to display the label of the group. * Renders a `<div>` element. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace GroupLabel { export type Props = useRender.ComponentProps<'div'>;}export function GroupLabel({ className, render, ...props }: GroupLabel.Props) { const defaultProps: useRender.ElementProps<'div'> & { 'data-slot'?: string; 'data-sidebar'?: string; } = { 'data-slot': 'sidebar-group-label', 'data-sidebar': 'group-label', className: cn( 'flex h-8 shrink-0 items-center rounded-md px-2 font-medium text-sidebar-foreground/70 text-xs outline-hidden ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', 'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0', className ), }; return useRender({ defaultTagName: 'div', render, props: mergeProps<'div'>(defaultProps, props), });}/** * A button that can be used to display the action of the group. * Renders a `<button>` element. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace GroupAction { export type Props = useRender.ComponentProps<'button'>;}export function GroupAction({ className, render, ...props }: GroupAction.Props) { const defaultProps: useRender.ElementProps<'button'> & { 'data-slot'?: string; 'data-sidebar'?: string; } = { 'data-slot': 'sidebar-group-action', 'data-sidebar': 'group-action', className: cn( 'absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', // Increases the hit area of the button on mobile. 'after:absolute after:-inset-2 md:after:hidden', 'group-data-[collapsible=icon]:hidden', className ), }; return useRender({ defaultTagName: 'button', render, props: mergeProps<'button'>(defaultProps, props), });}/** * A content that can be used to display the content of the group. * Renders a `<div>` element. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace GroupContent { export type Props = useRender.ComponentProps<'div'>;}export function GroupContent({ className, render, ...props }: GroupContent.Props) { const defaultProps: useRender.ElementProps<'div'> & { 'data-slot'?: string; 'data-sidebar'?: string; } = { 'data-slot': 'sidebar-group-content', 'data-sidebar': 'group-content', className: cn('w-full text-sm', className), }; return useRender({ defaultTagName: 'div', render, props: mergeProps<'div'>(defaultProps, props), });}/** * A menu that can be used for building a menu within a `Sidebar.Group`. * Renders a `<ul>` element. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace Menu { export type Props = useRender.ComponentProps<'ul'>;}export function Menu({ className, render, ...props }: Menu.Props) { const defaultProps: useRender.ElementProps<'ul'> & { 'data-slot'?: string; 'data-sidebar'?: string; } = { 'data-slot': 'sidebar-menu', 'data-sidebar': 'menu', className: cn('flex w-full min-w-0 flex-col gap-1', className), }; return useRender({ defaultTagName: 'ul', render, props: mergeProps<'ul'>(defaultProps, props), });}/** * Display an item of the menu. * Renders a `<li>` element. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace MenuItem { export type Props = useRender.ComponentProps<'li'>;}export function MenuItem({ className, render, ...props }: MenuItem.Props) { const defaultProps: useRender.ElementProps<'li'> & { 'data-slot'?: string; 'data-sidebar'?: string; } = { 'data-slot': 'sidebar-menu-item', 'data-sidebar': 'menu-item', className: cn('group/menu-item relative', className), }; return useRender({ defaultTagName: 'li', render, props: mergeProps<'li'>(defaultProps, props), });}const sidebarMenuButtonVariants = cva({ base: 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] 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 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-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', },});/** * A button that can be used to display the button of the menu item. * Renders a `<button>` element. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace MenuButton { export type Variants = VariantProps<typeof sidebarMenuButtonVariants>; export type State = { size: MenuButton.Variants['size']; isActive: boolean; }; export type Props = useRender.ComponentProps<'button'> & { /** * Whether the button is active. * @default false */ isActive?: boolean; /** * Tooltip positioner props or string to display a tooltip. */ tooltip?: string | React.ComponentProps<typeof Tooltip.Positioner>; } & MenuButton.Variants;}export function MenuButton({ isActive = false, variant = 'default', size = 'default', tooltip, render, className, ...props}: MenuButton.Props) { const { isMobile, state } = useSidebar(); const defaultProps: useRender.ElementProps<'button'> & { 'data-slot'?: string; 'data-sidebar'?: string; } = { 'data-slot': 'sidebar-menu-button', 'data-sidebar': 'menu-button', className: cn(sidebarMenuButtonVariants({ variant, size }), className), }; const button = useRender({ defaultTagName: 'button', render, state: { size, active: isActive, }, props: mergeProps<'button'>(defaultProps, props), }); if (!tooltip) { return button; } if (typeof tooltip === 'string') { tooltip = { children: tooltip, }; } return ( <Tooltip.Root> <Tooltip.Trigger render={button} /> <Tooltip.Positioner align="center" hidden={state !== 'collapsed' || isMobile} side="right" {...tooltip} > <Tooltip.Popup /> </Tooltip.Positioner> </Tooltip.Root> );}/** * A action that can be used to display the action of the menu item. * Renders a `<button>` element. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace MenuAction { export type Props = useRender.ComponentProps<'button'> & { /** * Whether the action should be shown on hover. * @default false */ showOnHover?: boolean; };}export function MenuAction({ className, render, showOnHover = false, ...props }: MenuAction.Props) { const defaultProps: useRender.ElementProps<'button'> & { 'data-slot'?: string; 'data-sidebar'?: string; } = { 'data-slot': 'sidebar-menu-action', 'data-sidebar': 'menu-action', className: cn( 'absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0', // Increases the hit area of the button on mobile. 'after:absolute after:-inset-2 md:after:hidden', 'peer-data-[size=sm]/menu-button:top-1', 'peer-data-[size=default]/menu-button:top-1.5', 'peer-data-[size=lg]/menu-button:top-2.5', 'group-data-[collapsible=icon]:hidden', showOnHover && 'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0', className ), }; return useRender({ defaultTagName: 'button', render, props: mergeProps<'button'>(defaultProps, props), });}/** * A badge that can be used to display the badge of the menu item. * Renders a `<div>` element. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace MenuBadge { export type Props = useRender.ComponentProps<'div'>;}export function MenuBadge({ className, render, ...props }: MenuBadge.Props) { const defaultProps: useRender.ElementProps<'div'> & { 'data-slot'?: string; 'data-sidebar'?: string; } = { 'data-slot': 'sidebar-menu-badge', 'data-sidebar': 'menu-badge', className: cn( 'pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 font-medium text-sidebar-foreground text-xs tabular-nums', 'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground', 'peer-data-[size=sm]/menu-button:top-1', 'peer-data-[size=default]/menu-button:top-1.5', 'peer-data-[size=lg]/menu-button:top-2.5', 'group-data-[collapsible=icon]:hidden', className ), }; return useRender({ defaultTagName: 'div', render, props: mergeProps<'div'>(defaultProps, props), });}/** * A skeleton that can be used to display the skeleton of the menu item. * Renders a `<div>` element. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace MenuSkeleton { export type Props = useRender.ComponentProps<'div'> & { /** * Whether the skeleton should show the icon. * @default false */ showIcon?: boolean; };}export function MenuSkeleton({ className, render, showIcon = false, ...props}: MenuSkeleton.Props) { // Random width between 50 to 90%. const width = React.useMemo(() => { return `${Math.floor(Math.random() * 40) + 50}%`; }, []); const defaultProps: useRender.ElementProps<'div'> & { 'data-slot'?: string; 'data-sidebar'?: string; } = { 'data-slot': 'sidebar-menu-skeleton', 'data-sidebar': 'menu-skeleton', className: cn('flex h-8 items-center gap-2 rounded-md px-2', className), children: ( <> {showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />} <Skeleton className="h-4 max-w-(--skeleton-width) flex-1" data-sidebar="menu-skeleton-text" style={ { '--skeleton-width': width, } as React.CSSProperties } /> </> ), }; return useRender({ defaultTagName: 'div', render, props: mergeProps<'div'>(defaultProps, props), });}/** * A submenu that can be used to display the submenu of the menu item. * Renders a `<ul>` element. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace MenuSub { export type Props = useRender.ComponentProps<'ul'>;}export function MenuSub({ className, render, ...props }: MenuSub.Props) { const defaultProps: useRender.ElementProps<'ul'> & { 'data-slot'?: string; 'data-sidebar'?: string; } = { 'data-slot': 'sidebar-menu-sub', 'data-sidebar': 'menu-sub', className: cn( 'mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-sidebar-border border-l px-2.5 py-0.5', 'group-data-[collapsible=icon]:hidden', className ), }; return useRender({ defaultTagName: 'ul', render, props: mergeProps<'ul'>(defaultProps, props), });}/** * A submenu item that can be used to display the submenu item of the submenu. * Renders a `<li>` element. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace MenuSubItem { export type Props = useRender.ComponentProps<'li'>;}export function MenuSubItem({ className, render, ...props }: MenuSubItem.Props) { const defaultProps: useRender.ElementProps<'li'> & { 'data-slot'?: string; 'data-sidebar'?: string; } = { 'data-slot': 'sidebar-menu-sub-item', 'data-sidebar': 'menu-sub-item', className: cn('group/menu-sub-item relative', className), }; return useRender({ defaultTagName: 'li', render, props: mergeProps<'li'>(defaultProps, props), });}/** * A submenu button that can be used to display the submenu button of the submenu item. * Renders a `<a>` element. * * Documentation: [Caprice UI Sidebar](https://caprice-ui.com/docs/components/sidebar) */export namespace MenuSubButton { export type Props = useRender.ComponentProps<'a'> & { /** * The size of the submenu button. * @default 'md' */ size?: 'sm' | 'md'; /** * Whether the submenu button is active. * @default false */ isActive?: boolean; };}export function MenuSubButton({ size = 'md', isActive = false, render, className, ...props}: MenuSubButton.Props) { const defaultProps: useRender.ElementProps<'a'> & { 'data-slot'?: string; 'data-sidebar'?: string; } = { 'data-slot': 'sidebar-menu-sub-button', 'data-sidebar': 'menu-sub-button', className: cn( 'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-hidden ring-sidebar-ring 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 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground', 'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground', size === 'sm' && 'text-xs', size === 'md' && 'text-sm', 'group-data-[collapsible=icon]:hidden', className ), }; return useRender({ defaultTagName: 'a', render, state: { active: isActive, size, }, props: mergeProps<'a'>(defaultProps, props), });}Update the import paths to match your project setup.
Structure
A Sidebar component is composed of the following parts:
Sidebar.Provider- Handles collapsible state.Sidebar.Root- The sidebar container.Sidebar.HeaderandSidebar.Footer- Sticky at the top and bottom of the sidebar.Sidebar.Content- Scrollable content.Sidebar.Group- Section within theSidebar.Content.Sidebar.Trigger- Trigger for theSidebar.Root.
Usage
import * as Sidebar from "@/components/caprice-ui/sidebar"import * as Sidebar from "@caprice-ui/react/sidebar"import * as Sidebar from "@/components/caprice-ui/sidebar"
import { AppSidebar } from "@/components/app-sidebar"
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<Sidebar.Provider>
<AppSidebar />
<main>
<Sidebar.Trigger />
{children}
</main>
</Sidebar.Provider>
)
}import * as Sidebar from "@/components/caprice-ui/sidebar"
export function AppSidebar() {
return (
<Sidebar.Root>
<Sidebar.Header />
<Sidebar.Content>
<Sidebar.Group />
<Sidebar.Group />
</Sidebar.Content>
<Sidebar.Footer />
</Sidebar.Root>
)
}Your First Sidebar
Let's start with the most basic sidebar. A collapsible sidebar with a menu.
Add a Sidebar.Provider and Sidebar.Trigger at the root of your application.
import * as Sidebar from "@/components/caprice-ui/sidebar";
import { AppSidebar } from "@/components/app-sidebar"
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<Sidebar.Provider>
<AppSidebar />
<main>
<Sidebar.Trigger />
{children}
</main>
</Sidebar.Provider>
)
}components/app-sidebar.tsx.import * as Sidebar from "@/components/caprice-ui/sidebar";
export function AppSidebar() {
return (
<Sidebar.Root>
<Sidebar.Content />
</Sidebar.Root>
)
}Sidebar.Menu to the sidebar.We'll use the Sidebar.Menu component in a Sidebar.Group.
import { Calendar, Home, Inbox, Search, Settings } from "lucide-react";
import * as Sidebar from "@/components/caprice-ui/sidebar";
// Menu items.
const items = [
{
title: "Home",
url: "#",
icon: Home,
},
{
title: "Inbox",
url: "#",
icon: Inbox,
},
{
title: "Calendar",
url: "#",
icon: Calendar,
},
{
title: "Search",
url: "#",
icon: Search,
},
{
title: "Settings",
url: "#",
icon: Settings,
},
]
export function AppSidebar() {
return (
<Sidebar.Root>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupLabel>Application</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
{items.map((item) => (
<Sidebar.MenuItem key={item.title}>
<Sidebar.MenuButton render={
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
} />
</Sidebar.MenuItem>
))}
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
</Sidebar.Content>
</Sidebar.Root>
)
}You should see something like this:
Your first sidebar.
Components
The components in sidebar.tsx are built to be composable i.e you build your sidebar by putting the provided components together. They also compose well with other shadcn/ui & Caprice UI components such as Menu, Collapsible or Dialog etc.
If you need to change the code in sidebar.tsx, you are encouraged to do so. The code is yours. Use sidebar.tsx as a starting point and build your own.
In the next sections, we'll go over each component and how to use them.
Sidebar.Provider
The Sidebar.Provider component is used to provide the sidebar context to the Sidebar.Root component. You should always wrap your application in a Sidebar.Provider component.
Props
| Name | Type | Description |
|---|---|---|
defaultOpen | boolean | Default open state of the sidebar. |
open | boolean | Open state of the sidebar (controlled). |
onOpenChange | (open: boolean) => void | Sets open state of the sidebar (controlled). |
Width
If you have a single sidebar in your application, you can use the SIDEBAR_WIDTH and SIDEBAR_WIDTH_MOBILE variables in sidebar.tsx to set the width of the sidebar.
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"For multiple sidebars in your application, you can use the style prop to set the width of the sidebar.
To set the width of the sidebar, you can use the --sidebar-width and --sidebar-width-mobile CSS variables in the style prop.
<Sidebar.Provider
style={{
"--sidebar-width": "20rem",
"--sidebar-width-mobile": "20rem",
}}
>
<Sidebar.Root />
</Sidebar.Provider>This will handle the width of the sidebar but also the layout spacing.
Keyboard Shortcut
The SIDEBAR_KEYBOARD_SHORTCUT variable is used to set the keyboard shortcut used to open and close the sidebar.
To trigger the sidebar, you use the cmd+b keyboard shortcut on Mac and ctrl+b on Windows.
You can change the keyboard shortcut by updating the SIDEBAR_KEYBOARD_SHORTCUT variable.
const SIDEBAR_KEYBOARD_SHORTCUT = "b"Persisted State
The Sidebar.Provider supports persisting the sidebar state across page reloads and server-side rendering. It uses cookies to store the current state of the sidebar. When the sidebar state changes, a default cookie named sidebar_state is set with the current open/closed state. This cookie is then read on subsequent page loads to restore the sidebar state.
To persist sidebar state in Next.js, set up your Sidebar.Provider in app/layout.tsx like this:
import { cookies } from "next/headers"
import * as Sidebar from "@/components/caprice-ui/sidebar";
import { AppSidebar } from "@/components/app-sidebar"
export async function Layout({ children }: { children: React.ReactNode }) {
const cookieStore = await cookies()
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"
return (
<Sidebar.Provider defaultOpen={defaultOpen}>
<AppSidebar />
<main>
<Sidebar.Trigger />
{children}
</main>
</Sidebar.Provider>
)
}You can change the name of the cookie by updating the SIDEBAR_COOKIE_NAME variable in sidebar.tsx.
const SIDEBAR_COOKIE_NAME = "sidebar_state"Sidebar.Root
The main Sidebar.Root component used to render a collapsible sidebar.
import * as Sidebar from "@/components/caprice-ui/sidebar";
export function AppSidebar() {
return <Sidebar.Root />
}Props
| Property | Type | Description |
|---|---|---|
side | left or right | The side of the sidebar. |
variant | sidebar, floating, or inset | The variant of the sidebar. |
collapsible | offcanvas, icon, or none | Collapsible state of the sidebar. |
side
Use the side prop to change the side of the sidebar.
Available options are left and right.
import * as Sidebar from "@/components/caprice-ui/sidebar";
export function AppSidebar() {
return <Sidebar.Root side="left | right" />
}variant
Use the variant prop to change the variant of the sidebar.
Available options are sidebar, floating and inset.
import * as Sidebar from "@/components/caprice-ui/sidebar";
export function AppSidebar() {
return <Sidebar.Root variant="sidebar | floating | inset" />
}Note: If you use the inset variant, remember to wrap your main content
in a SidebarInset component.
<Sidebar.Provider>
<Sidebar.Root variant="inset" />
<Sidebar.Inset>
<main>{children}</main>
</Sidebar.Inset>
</Sidebar.Provider>collapsible
Use the collapsible prop to make the sidebar collapsible.
Available options are offcanvas, icon and none.
import * as Sidebar from "@/components/caprice-ui/sidebar";
export function AppSidebar() {
return <Sidebar.Root collapsible="offcanvas | icon | none" />
}| Prop | Description |
|---|---|
offcanvas | A collapsible sidebar that slides in from the left or right. |
icon | A sidebar that collapses to icons. |
none | A non-collapsible sidebar. |
useSidebar
The useSidebar hook is used to control the sidebar.
import { useSidebar } from "@/components/ui/sidebar"
export function AppSidebar() {
const {
state,
open,
setOpen,
openMobile,
setOpenMobile,
isMobile,
toggleSidebar,
} = useSidebar()
}| Property | Type | Description |
|---|---|---|
state | expanded or collapsed | The current state of the sidebar. |
open | boolean | Whether the sidebar is open. |
setOpen | (open: boolean) => void | Sets the open state of the sidebar. |
openMobile | boolean | Whether the sidebar is open on mobile. |
setOpenMobile | (open: boolean) => void | Sets the open state of the sidebar on mobile. |
isMobile | boolean | Whether the sidebar is on mobile. |
toggleSidebar | () => void | Toggles the sidebar. Desktop and mobile. |
Sidebar.Header
Use the Sidebar.Header component to add a sticky header to the sidebar.
The following example adds a <Menu> to the Sidebar.Header.
A sidebar header with a dropdown menu.
<Sidebar.Root>
<Sidebar.Header>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Menu.Root>
<Menu.Trigger render={
<Sidebar.MenuButton>
Select Workspace
<ChevronDown className="ml-auto" />
</Sidebar.MenuButton>
} />
<Menu.Positioner>
<Menu.Popup className="w-(--anchor-width)">
<Menu.Item>
<span>Acme Inc</span>
</Menu.Item>
<Menu.Item>
<span>Acme Corp.</span>
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Root>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Header>
</Sidebar.Root>Sidebar.Footer
Use the Sidebar.Footer component to add a sticky footer to the sidebar.
The following example adds a <Menu> to the Sidebar.Footer.
A sidebar footer with a dropdown menu.
export function AppSidebar() {
return (
<Sidebar.Provider>
<Sidebar.Root>
<Sidebar.Header />
<Sidebar.Content />
<Sidebar.Footer>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Menu.Root>
<Menu.Trigger render={
<Sidebar.MenuButton>
<User2 /> Username
<ChevronUp className="ml-auto" />
</Sidebar.MenuButton>
} />
<Menu.Positioner side="top">
<Menu.Popup className="w-(--anchor-width)">
<Menu.Item>
<span>Account</span>
</Menu.Item>
<Menu.Item>
<span>Billing</span>
</Menu.Item>
<Menu.Item>
<span>Sign out</span>
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Root>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Footer>
</Sidebar.Root>
</Sidebar.Provider>
)
}Sidebar.Content
The Sidebar.Content component is used to wrap the content of the sidebar. This is where you add your Sidebar.Group components. It is scrollable.
import * as Sidebar from "@/components/caprice-ui/sidebar";
export function AppSidebar() {
return (
<Sidebar.Root>
<Sidebar.Positioner>
<Sidebar.Content>
<Sidebar.Group />
<Sidebar.Group />
</Sidebar.Content>
</Sidebar.Positioner>
</Sidebar.Root>
)
}Sidebar.Group
Use the Sidebar.Group component to create a section within the sidebar.
A Sidebar.Group has a Sidebar.GroupLabel, a Sidebar.GroupContent and an optional Sidebar.GroupAction.
A sidebar group.
import * as Sidebar from "@/components/caprice-ui/sidebar";
export function AppSidebar() {
return (
<Sidebar.Root>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupLabel>Application</Sidebar.GroupLabel>
<Sidebar.GroupAction>
<Plus /> <span className="sr-only">Add Project</span>
</Sidebar.GroupAction>
<Sidebar.GroupContent></Sidebar.GroupContent>
</Sidebar.Group>
</Sidebar.Content>
</Sidebar.Root>
)
}Collapsible Sidebar.Group
To make a Sidebar.Group collapsible, wrap it in a Collapsible.
A collapsible sidebar group.
export function AppSidebar() {
return (
<Collapsible.Root defaultOpen className="group/collapsible">
<Sidebar.Group>
<Sidebar.GroupLabel render={
<Collapsible.Trigger>
Help
<ChevronDown className="ml-auto transition-transform group-data-open/collapsible:rotate-180" />
</Collapsible.Trigger>
} />
<CollapsibleContent>
<Sidebar.GroupContent />
</CollapsibleContent>
</Sidebar.Group>
</Collapsible.Root>
)
}Note: We wrap the Collapsible.Trigger in a Sidebar.GroupLabel to render
a button.
SidebarGroupAction
Use the SidebarGroupAction component to add an action button to the SidebarGroup.
A sidebar group with an action button.
export function AppSidebar() {
return (
<Sidebar.Group>
<Sidebar.GroupLabel>Projects</Sidebar.GroupLabel>
<Sidebar.GroupAction title="Add Project">
<Plus /> <span className="sr-only">Add Project</span>
</Sidebar.GroupAction>
<Sidebar.GroupContent />
</Sidebar.Group>
)
}Sidebar.Menu
The Sidebar.Menu component is used for building a menu within a Sidebar.Group.
A Sidebar.Menu component is composed of Sidebar.MenuItem, Sidebar.MenuButton, <Sidebar.MenuAction /> and <Sidebar.MenuSub /> components.
Here's an example of a Sidebar.Menu component rendering a list of projects.
A sidebar menu with a list of projects.
<Sidebar.Root>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupLabel>Projects</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
{projects.map((project) => (
<Sidebar.MenuItem key={project.name}>
<Sidebar.MenuButton
render={
<a href={project.url}>
<project.icon />
<span>{project.name}</span>
</a>
}
/>
</Sidebar.MenuItem>
))}
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
</Sidebar.Content>
</Sidebar.Root>Sidebar.MenuButton
The Sidebar.MenuButton component is used to render a menu button within a Sidebar.MenuItem.
Link or Anchor
By default, the Sidebar.MenuButton renders a button but you can use the render prop to render a different component such as a Link or an a tag.
<Sidebar.MenuButton render={<a href="#">Home</a>} />Icon and Label
You can render an icon and a truncated label inside the button. Remember to wrap the label in a <span>.
<Sidebar.MenuButton
render={
<a href="#">
<Home />
<span>Home</span>
</a>
}
/>isActive
Use the isActive prop to mark a menu item as active.
<Sidebar.MenuButton isActive render={<a href="#">Home</a>} />Sidebar.MenuAction
The Sidebar.MenuAction component is used to render a menu action within a Sidebar.MenuItem.
This button works independently of the Sidebar.MenuButton i.e you can have the <Sidebar.MenuButton /> as a clickable link and the <Sidebar.MenuAction /> as a button.
<Sidebar.MenuItem>
<Sidebar.MenuButton
render={
<a href="#">
<Home />
<span>Home</span>
</a>
}
/>
<Sidebar.MenuAction>
<Plus /> <span className="sr-only">Add Project</span>
</Sidebar.MenuAction>
</Sidebar.MenuItem>Menu
Here's an example of a Sidebar.MenuAction component rendering a Menu.
A sidebar menu action with a dropdown menu.
<Sidebar.MenuItem>
<Sidebar.MenuButton
className="group-has-data-popup-open/menu-item:bg-sidebar-accent"
render={
<a href="#">
<Home />
<span>Home</span>
</a>
}
/>
<Menu.Root>
<Menu.Trigger
render={
<Sidebar.MenuAction>
<MoreHorizontalIcon />
<span className="sr-only">More</span>
</Sidebar.MenuAction>
}
/>
<Menu.Positioner align="start" side="right">
<Menu.Popup>
<Menu.Item>
<span>Edit Project</span>
</Menu.Item>
<Menu.Item>
<span>Delete Project</span>
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Root>
</Sidebar.MenuItem>Sidebar.MenuSub
The Sidebar.MenuSub component is used to render a submenu within a Sidebar.Menu.
Use <Sidebar.MenuSubItem /> and <Sidebar.MenuSubButton /> to render a submenu item.
A sidebar menu with a submenu.
<Sidebar.MenuItem>
<Sidebar.MenuButton />
<Sidebar.MenuSub>
<Sidebar.MenuSubItem>
<Sidebar.MenuSubButton />
</Sidebar.MenuSubItem>
<Sidebar.MenuSubItem>
<Sidebar.MenuSubButton />
</Sidebar.MenuSubItem>
</Sidebar.MenuSub>
</Sidebar.MenuItem>Collapsible Sidebar.Menu
To make a Sidebar.Menu component collapsible, wrap it and the Sidebar.MenuSub components in a Collapsible.
A collapsible sidebar menu.
<Sidebar.Menu>
<Collapsible.Root defaultOpen className="group/collapsible">
<Sidebar.MenuItem>
<Collapsible.Trigger render={<Sidebar.MenuButton />} />
<Collapsible.Panel>
<Sidebar.MenuSub>
<Sidebar.MenuSubItem />
</Sidebar.MenuSub>
</Collapsible.Panel>
</Sidebar.MenuItem>
</Collapsible.Root>
</Sidebar.Menu>Sidebar.MenuBadge
The Sidebar.MenuBadge component is used to render a badge within a Sidebar.MenuItem.
A sidebar menu with a badge.
<Sidebar.MenuItem>
<Sidebar.MenuButton />
<Sidebar.MenuBadge>24</Sidebar.MenuBadge>
</Sidebar.MenuItem>Sidebar.MenuSkeleton
The Sidebar.MenuSkeleton component is used to render a skeleton for a Sidebar.Menu. You can use this to show a loading state when using React Server Components, SWR or react-query.
function NavProjectsSkeleton() {
return (
<Sidebar.Menu>
{Array.from({ length: 5 }).map((_, index) => (
<Sidebar.MenuItem key={index}>
<Sidebar.MenuSkeleton />
</Sidebar.MenuItem>
))}
</Sidebar.Menu>
)
}Sidebar.Separator
The Sidebar.Separator component is used to render a separator within a Sidebar.Root.
<Sidebar.Root>
<Sidebar.Header />
<Sidebar.Separator />
<Sidebar.Content>
<Sidebar.Group />
<Sidebar.Separator />
<Sidebar.Group />
</Sidebar.Content>
</Sidebar.Root>Sidebar.Trigger
Use the Sidebar.Trigger component to render a button that toggles the sidebar.
The Sidebar.Trigger component must be used within a Sidebar.Provider.
<Sidebar.Provider>
<Sidebar.Root />
<main>
<Sidebar.Trigger />
</main>
</Sidebar.Provider>Custom Trigger
To create a custom trigger, you can use the useSidebar hook.
import { useSidebar } from "@/components/caprice-ui/sidebar"
export function CustomTrigger() {
const { toggleSidebar } = useSidebar()
return <button onClick={toggleSidebar}>Toggle Sidebar</button>
}Sidebar.Rail
The Sidebar.Rail component is used to render a rail within a Sidebar.Root. This rail can be used to toggle the sidebar.
<Sidebar.Root>
<Sidebar.Header />
<Sidebar.Content>
<Sidebar.Group />
</Sidebar.Content>
<Sidebar.Footer />
<Sidebar.Rail />
</Sidebar.Root>Data Fetching
React Server Components
Here's an example of a Sidebar.Menu component rendering a list of projects using React Server Components.
A sidebar menu using React Server Components.
function NavProjectsSkeleton() {
return (
<SidebarMenu>
{Array.from({ length: 5 }).map((_, index) => (
<SidebarMenuItem key={index}>
<SidebarMenuSkeleton showIcon />
</SidebarMenuItem>
))}
</SidebarMenu>
)
}async function NavProjects() {
const projects = await fetchProjects()
return (
<Sidebar.Menu>
{projects.map((project) => (
<Sidebar.MenuItem key={project.name}>
<Sidebar.MenuButton render={
<a href={project.url}>
<project.icon />
<span>{project.name}</span>
</a>} />
</Sidebar.MenuItem>
))}
</Sidebar.Menu>
)
}function AppSidebar() {
return (
<Sidebar.Root>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupLabel>Projects</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<React.Suspense fallback={<NavProjectsSkeleton />}>
<NavProjects />
</React.Suspense>
</Sidebar.GroupContent>
</Sidebar.Group>
</Sidebar.Content>
</Sidebar.Root>
)
}SWR and React Query
You can use the same approach with SWR or react-query.
function NavProjects() {
const { data, isLoading } = useSWR("/api/projects", fetcher)
if (isLoading) {
return (
<Sidebar.Menu>
{Array.from({ length: 5 }).map((_, index) => (
<Sidebar.MenuItem key={index}>
<Sidebar.MenuSkeleton showIcon />
</Sidebar.MenuItem>
))}
</Sidebar.Menu>
)
}
if (!data) {
return ...
}
return (
<Sidebar.Menu>
{data.map((project) => (
<Sidebar.MenuItem key={project.name}>
<Sidebar.MenuButton render={
<a href={project.url}>
<project.icon />
<span>{project.name}</span>
</a>
} />
</Sidebar.MenuItem>
))}
</Sidebar.Menu>
)
}function NavProjects() {
const { data, isLoading } = useQuery()
if (isLoading) {
return (
<Sidebar.Menu>
{Array.from({ length: 5 }).map((_, index) => (
<Sidebar.MenuItem key={index}>
<Sidebar.MenuSkeleton showIcon />
</Sidebar.MenuItem>
))}
</Sidebar.Menu>
)
}
if (!data) {
return ...
}
return (
<Sidebar.Menu>
{data.map((project) => (
<Sidebar.MenuItem key={project.name}>
<Sidebar.MenuButton render={
<a href={project.url}>
<project.icon />
<span>{project.name}</span>
</a>
} />
</Sidebar.MenuItem>
))}
</Sidebar.Menu>
)
}Controlled Sidebar
Use the open and onOpenChange props to control the sidebar.
A controlled sidebar.
export function AppSidebar() {
const [open, setOpen] = React.useState(false)
return (
<Sidebar.Provider open={open} onOpenChange={setOpen}>
<Sidebar.Root />
</Sidebar.Provider>
)
}Theming
We use the following CSS variables to theme the sidebar.
@layer base {
:root {
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 0 0% 98%;
--sidebar-primary-foreground: 240 5.9% 10%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}We intentionally use different variables for the sidebar and the rest of the application to make it easy to have a sidebar that is styled differently from the rest of the application. Think a sidebar with a darker shade from the main application.
Styling
Here are some tips for styling the sidebar based on different states.
- Styling an element based on the sidebar collapsible state. The following will hide the
SidebarGroupwhen the sidebar is iniconmode.
<Sidebar.Root collapsible="icon">
<Sidebar.Content>
<Sidebar.Group className="group-data-[collapsible=icon]:hidden" />
</Sidebar.Content>
</Sidebar.Root>- Styling a menu action based on the menu button active state. The following will force the menu action to be visible when the menu button is active.
<Sidebar.MenuItem>
<Sidebar.MenuButton />
<Sidebar.MenuAction className="peer-data-[active=true]/menu-button:opacity-100" />
</Sidebar.MenuItem>You can find more tips on using states for styling in this Twitter thread.