Pallas UI
DocsComponents
Core Concepts
    • Introduction
    • Getting Started
    • Theming
    • Color Tokens
    • Spacing & Sizing
    • Layout Guide
    • AspectRatio
    • Box
    • Flex
    • Grid
    • Shapes
Previews
    • Accordion
    • Alert
    • Avatar
    • Badge
    • Breadcrumb
    • Button
    • Carousel
    • Checkbox
    • Combobox
    • Command
    • Date Picker
    • Form
    • Input
    • Input OTP
    • Label
    • MenuBar
    • Modal
    • Popover
    • Progress
    • Radio Group
    • Segmented
    • Select
    • Separator
    • Sheet
    • Sidebar
    • Skeleton
    • Slider
    • Spinner
    • Steps
    • Switch
    • Tabs
    • Textarea
    • Toast
    • Tooltip
    • Typography
  1. Components
  2. Sidebar

Sidebar

A flexible and accessible sidebar navigation component built with Radix UI primitives.

  • Acme IncEnterprise
Application
  • 12
    • Users
    • Groups
    • Media
  • Pallas UICarbonteq

Installation

Install the following dependencies

npm install @radix-ui/react-slot

Copy and paste the following code into your project

provider.tsx:

import { Provider as ProviderPrimitive, type SidebarProviderProps } from '@pallas-ui/sidebar'
import { createStyleContext } from '@pallas-ui/style-context'
import { cx } from '@styled-system/css'
import { sidebar } from '@styled-system/recipes'
import type { Assign, JsxStyleProps } from '@styled-system/types'
import React from 'react'
import Tooltip from '../tooltip/tooltip'
 
export const { withProvider, withContext } = createStyleContext(sidebar)
 
const ProviderStyled = withProvider<
  React.ComponentRef<typeof ProviderPrimitive>,
  Assign<SidebarProviderProps, JsxStyleProps>
>(ProviderPrimitive, 'provider')
 
export const Provider = React.forwardRef<
  React.ComponentRef<typeof ProviderStyled>,
  SidebarProviderProps
>(({ className, style, children, ...props }, ref) => {
  return (
    <Tooltip.Provider delayDuration={0}>
      <ProviderStyled className={cx('group/sidebar-wrapper', className)} ref={ref} {...props}>
        {children}
      </ProviderStyled>
    </Tooltip.Provider>
  )
})

root.tsx:

import { useSidebar } from '@pallas-ui/sidebar'
import {
  RootCollapsible,
  RootFixed,
  RootGap,
  RootInner,
  RootNonCollapsible,
} from '@pallas-ui/sidebar'
import { css } from '@styled-system/css'
import React from 'react'
import Drawer from '../drawer'
import { withContext } from './provider'
 
export type SidebarRootProps = React.ComponentPropsWithoutRef<'div'> & {
  side?: 'left' | 'right'
  variant?: 'sidebar' | 'floating' | 'inset'
  collapsible?: 'offcanvas' | 'icon' | 'none'
}
 
const RootCollapsibleStyled = withContext<
  React.ComponentRef<typeof RootCollapsible>,
  React.ComponentProps<typeof RootCollapsible>
>(RootCollapsible, 'root')
 
const RootNonCollapsibleStyled = withContext<
  React.ComponentRef<typeof RootNonCollapsible>,
  React.ComponentProps<typeof RootNonCollapsible>
>(RootNonCollapsible, 'rootNonCollapsible')
 
const GapStyled = withContext<
  React.ComponentRef<typeof RootGap>,
  React.ComponentProps<typeof RootGap>
>(RootGap, 'gap')
 
const FixedStyled = withContext<
  React.ComponentRef<typeof RootFixed>,
  React.ComponentProps<typeof RootFixed>
>(RootFixed, 'fixed')
 
const InnerStyled = withContext<
  React.ComponentRef<typeof RootInner>,
  React.ComponentProps<typeof RootInner>
>(RootInner, 'inner')
 
export const Root = React.forwardRef<
  React.ComponentRef<typeof RootNonCollapsibleStyled>,
  SidebarRootProps
>(({ side = 'left', variant = 'sidebar', collapsible = 'offcanvas', children, ...props }, ref) => {
  const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
 
  if (isMobile) {
    return (
      <Drawer.Root open={openMobile} onOpenChange={setOpenMobile} {...props} side={side}>
        <Drawer.Content data-sidebar="sidebar" data-mobile="true">
          <Drawer.Header className={css({ srOnly: true })}>
            <Drawer.Title>Sidebar</Drawer.Title>
            <Drawer.Description>Displays the mobile sidebar.</Drawer.Description>
          </Drawer.Header>
          <Drawer.Body
            css={{
              px: 0,
            }}
          >
            {children}
          </Drawer.Body>
        </Drawer.Content>
      </Drawer.Root>
    )
  }
  if (collapsible === 'none') {
    return (
      <RootNonCollapsibleStyled ref={ref} {...props}>
        {children}
      </RootNonCollapsibleStyled>
    )
  }
 
  return (
    <RootCollapsibleStyled
      ref={ref}
      className="group peer"
      data-state={state}
      data-collapsible={state === 'collapsed' ? collapsible : ''}
      data-variant={variant}
      data-side={side}
    >
      {/* This is what handles the sidebar gap on desktop */}
      <GapStyled />
      <FixedStyled {...props}>
        <InnerStyled>{children}</InnerStyled>
      </FixedStyled>
    </RootCollapsibleStyled>
  )
})
Root.displayName = 'Sidebar'

content.tsx:

import { Content as ContentPrimitive } from '@pallas-ui/sidebar'
import type { Assign } from '@pallas-ui/style-context'
import type { JsxStyleProps } from '@styled-system/types'
import type React from 'react'
import { withContext } from './provider'
 
export const Content = withContext<
  React.ComponentRef<typeof ContentPrimitive>,
  Assign<React.ComponentProps<typeof ContentPrimitive>, JsxStyleProps>
>(ContentPrimitive, 'content')

inset.tsx:

import { Inset as InsetPrimitive } from '@pallas-ui/sidebar'
import type { Assign, JsxStyleProps } from '@styled-system/types'
import type React from 'react'
import { withContext } from './provider'
 
export const Inset = withContext<
  React.ComponentRef<'main'>,
  Assign<React.ComponentProps<typeof InsetPrimitive>, JsxStyleProps>
>(InsetPrimitive, 'inset')

header.tsx:

import { Header as HeaderPrimitive } from '@pallas-ui/sidebar'
import type { Assign } from '@pallas-ui/style-context'
import type { JsxStyleProps } from '@styled-system/types'
import type React from 'react'
import { withContext } from './provider'
 
export const Header = withContext<
  React.ComponentRef<typeof HeaderPrimitive>,
  Assign<React.ComponentProps<typeof HeaderPrimitive>, JsxStyleProps>
>(HeaderPrimitive, 'header')

footer.tsx:

import { Footer as FooterPrimitive } from '@pallas-ui/sidebar'
import type { Assign, JsxStyleProps } from '@styled-system/types'
import type React from 'react'
import { withContext } from './provider'
 
export const Footer = withContext<
  React.ComponentRef<typeof FooterPrimitive>,
  Assign<React.ComponentProps<typeof FooterPrimitive>, JsxStyleProps>
>(FooterPrimitive, 'footer')

group.tsx:

import {
  GroupAction as GroupActionPrimitive,
  GroupContent as GroupContentPrimitive,
  GroupLabel as GroupLabelPrimitive,
  Group as GroupPrimitive,
  type SidebarGroupActionProps,
  type SidebarGroupLabelProps,
} from '@pallas-ui/sidebar'
import { cx } from '@styled-system/css'
import { button } from '@styled-system/recipes'
import type { Assign, JsxStyleProps } from '@styled-system/types'
import React from 'react'
import type { ButtonProps } from '../button'
import { withContext } from './provider'
 
export const Group = withContext<
  React.ComponentRef<typeof GroupPrimitive>,
  Assign<React.ComponentProps<typeof GroupPrimitive>, JsxStyleProps>
>(GroupPrimitive, 'group')
 
export const GroupLabel = withContext<
  React.ComponentRef<typeof GroupLabelPrimitive>,
  Assign<SidebarGroupLabelProps, JsxStyleProps>
>(GroupLabelPrimitive, 'groupLabel')
 
type ActionButtonProps = Assign<SidebarGroupActionProps, ButtonProps>
const GroupActionStyled = withContext<
  React.ComponentRef<typeof GroupActionPrimitive>,
  ActionButtonProps
>(GroupActionPrimitive, 'groupAction')
 
export const GroupAction = React.forwardRef<
  React.ComponentRef<typeof GroupActionStyled>,
  ActionButtonProps
>((props, ref) => {
  const [buttonProps, { className, ...rest }] = button.splitVariantProps(props)
  return <GroupActionStyled ref={ref} className={cx(button(buttonProps), className)} {...rest} />
})
 
export const GroupContent = withContext<
  React.ComponentRef<typeof GroupContentPrimitive>,
  Assign<SidebarGroupLabelProps, JsxStyleProps>
>(GroupContentPrimitive, 'groupContent')

menu.tsx:

import {
  MenuAction as MenuActionPrimitive,
  type MenuActionProps,
  MenuBadge as MenuBadgePrimitive,
  MenuButton as MenuButtonPrimitive,
  type MenuButtonProps,
  MenuItem as MenuItemPrimitive,
  Menu as MenuPrimitive,
  useSidebar,
} from '@pallas-ui/sidebar'
import type { Assign } from '@pallas-ui/style-context'
import { Slot } from '@radix-ui/react-slot'
import { cx } from '@styled-system/css'
import { button } from '@styled-system/recipes'
import type { JsxStyleProps } from '@styled-system/types'
import React from 'react'
import type { ButtonProps } from '../button'
import Tooltip from '../tooltip/tooltip'
import { withContext } from './provider'
 
export const Menu = withContext<
  React.ComponentRef<typeof MenuPrimitive>,
  Assign<React.ComponentProps<typeof MenuPrimitive>, JsxStyleProps>
>(MenuPrimitive, 'menu')
 
type MenuItemProps = Assign<React.ComponentProps<typeof MenuItemPrimitive>, JsxStyleProps>
const MenuItemStyled = withContext<React.ComponentRef<typeof MenuItemPrimitive>, MenuItemProps>(
  MenuItemPrimitive,
  'menuItem',
)
export const MenuItem = React.forwardRef<
  React.ComponentRef<typeof MenuItemPrimitive>,
  MenuItemProps
>(({ className, ...props }, ref) => (
  <MenuItemStyled ref={ref} className={cx('group/menu-item', className)} {...props} />
))
 
// const sidebarMenuButtonVariants = cva({
//   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",
// },
//     variant: {
//       default: {
//         _hover: {
//           bg: 'sidebar-accent',
//           color: 'sidebar-accent-foreground',
//         },
//       },
//       outline: {
//         bg: 'background',
//         boxShadow: '0 0 0 1px hsl(var(--sidebar-border))',
//         _hover: {
//           bg: 'sidebar-accent',
//           color: 'sidebar-accent-foreground',
//           boxShadow: '0 0 0 1px hsl(var(--sidebar-accent))',
//         },
//       },
//     },
//     size: {
//       default: {
//         height: '{sizes.lg}',
//         fontSize: '{fontSizes.sm}',
//       },
//       sm: {
//         height: '{sizes.sm}',
//         fontSize: '{fontSizes.xs}',
//       },
//       lg: {
//         height: '{sizes.md}',
//         fontSize: '{fontSizes.md}',
//         'group-data-[collapsible=icon]': {
//           padding: '0!',
//         },
//       },
//     },
//   },
//   defaultVariants: {
//     variant: 'default',
//     size: 'default',
//   },
// })
 
type SidebarMenuButtonProps = Assign<MenuButtonProps, ButtonProps> & {
  tooltip?: string | React.ComponentProps<typeof Tooltip.Content>
}
const MenuButtonStyled = withContext<
  React.ComponentRef<typeof MenuButtonPrimitive>,
  SidebarMenuButtonProps
>(MenuButtonPrimitive, 'menuButton')
 
export const MenuButton = React.forwardRef<
  React.ComponentRef<typeof MenuButtonPrimitive>,
  SidebarMenuButtonProps
>((props, ref) => {
  let [buttonProps, { asChild = false, isActive = false, tooltip, className, ...rest }] =
    button.splitVariantProps(props)
  const Comp = asChild ? Slot : MenuButtonStyled
  const { isMobile, state } = useSidebar()
 
  const Button = (
    <Comp
      ref={ref}
      className={cx('menu-button', button({ variant: 'text', ...buttonProps }), className)}
      {...rest}
    />
  )
 
  if (!tooltip) {
    return Button
  }
 
  if (typeof tooltip === 'string') {
    tooltip = {
      children: tooltip,
    }
  }
 
  return (
    <Tooltip.Root>
      <Tooltip.Trigger asChild>{Button}</Tooltip.Trigger>
      <Tooltip.Content
        side="right"
        align="center"
        hidden={state !== 'collapsed' || isMobile}
        {...tooltip}
      />
    </Tooltip.Root>
  )
})
 
type SidebarMenuActionProps = Assign<MenuActionProps, ButtonProps>
const SidebarMenuActionStyled = withContext<
  React.ComponentRef<typeof MenuActionPrimitive>,
  SidebarMenuActionProps
>(MenuActionPrimitive, 'menuAction')
export const MenuAction = React.forwardRef<
  React.ComponentRef<typeof MenuActionPrimitive>,
  SidebarMenuActionProps
>((props, ref) => {
  const [buttonVariantProps, { className, ...rest }] = button.splitVariantProps(props)
  return (
    <SidebarMenuActionStyled
      ref={ref}
      className={cx(button(buttonVariantProps), className)}
      {...rest}
    />
  )
})
 
export const MenuBadge = withContext<
  React.ComponentRef<typeof MenuBadgePrimitive>,
  Assign<React.ComponentProps<typeof MenuBadgePrimitive>, JsxStyleProps>
>(MenuBadgePrimitive, 'menuBadge')

subMenu.tsx:

import {
  MenuSubButton as MenuSubButtomPrimitive,
  type MenuSubButtonProps,
  MenuSubItem as MenuSubItemPrimitive,
  MenuSub as MenuSubPrimitive,
} from '@pallas-ui/sidebar'
import type { Assign } from '@pallas-ui/style-context'
import type { JsxStyleProps } from '@styled-system/types'
import type React from 'react'
import { withContext } from './provider'
 
export const MenuSub = withContext<
  React.ComponentRef<typeof MenuSubPrimitive>,
  Assign<React.ComponentProps<typeof MenuSubPrimitive>, JsxStyleProps>
>(MenuSubPrimitive, 'menuSub')
 
export const MenuSubItem = withContext<
  React.ComponentRef<typeof MenuSubItemPrimitive>,
  Assign<React.ComponentProps<typeof MenuSubItemPrimitive>, JsxStyleProps>
>(MenuSubItemPrimitive, 'menuSubItem')
 
export const MenuSubButton = withContext<
  React.ComponentRef<typeof MenuSubButtomPrimitive>,
  Assign<MenuSubButtonProps, JsxStyleProps>
>(MenuSubButtomPrimitive, 'menuSubButton')

rail.tsx:

import { Rail as RailPrimitive } from '@pallas-ui/sidebar'
import type { Assign, JsxStyleProps } from '@styled-system/types'
import { withContext } from './provider'
 
export const Rail = withContext<
  React.ComponentRef<typeof RailPrimitive>,
  Assign<React.ComponentProps<typeof RailPrimitive>, JsxStyleProps>
>(RailPrimitive, 'rail')

separator.tsx:

import { Separator as SeparatorPrimitive } from '@pallas-ui/sidebar'
import { cx } from '@styled-system/css'
import { type SeparatorVariantProps, separator } from '@styled-system/recipes'
import { withContext } from './provider'
 
type SideSeparatorProps = React.ComponentProps<typeof SeparatorPrimitive> & SeparatorVariantProps
 
const SeparatorStyled = withContext<
  React.ComponentRef<typeof SeparatorPrimitive>,
  SideSeparatorProps
>(SeparatorPrimitive, 'separator')
 
export const Separator = (props: SideSeparatorProps) => {
  const [separatorProps, { className, ...rest }] = separator.splitVariantProps(props)
  return <SeparatorStyled className={cx(separator(separatorProps), className)} {...rest} />
}

trigger.tsx:

import { Trigger as TriggerPrimitive } from '@pallas-ui/sidebar'
import { css, cx } from '@styled-system/css'
import { button } from '@styled-system/recipes'
import type { Assign } from '@styled-system/types'
import React from 'react'
import type { ButtonProps } from '../button'
import { withContext } from './provider'
 
type TriggerProps = Assign<React.ComponentProps<typeof TriggerPrimitive>, ButtonProps>
 
const TriggerStyled = withContext<React.ComponentRef<typeof TriggerPrimitive>, TriggerProps>(
  TriggerPrimitive,
  'trigger',
)
 
export const Trigger = React.forwardRef<React.ComponentRef<typeof TriggerPrimitive>, TriggerProps>(
  (props, ref) => {
    const [buttonProps, { className, children, ...restProps }] = button.splitVariantProps(props)
    return (
      <TriggerStyled
        ref={ref}
        className={cx(button({ variant: 'text', ...buttonProps }), className)}
        {...restProps}
      >
        {children}
        <span className={css({ srOnly: true })}>Toggle Sidebar</span>
      </TriggerStyled>
    )
  },
)
Trigger.displayName = 'SidebarTrigger'

index.tsx:

'use client'
 
import { useSidebar } from '@pallas-ui/sidebar'
import { Content } from './content'
import { Footer } from './footer'
import { Group, GroupAction, GroupContent, GroupLabel } from './group'
import { Header } from './header'
import { Inset } from './inset'
import { Menu, MenuAction, MenuBadge, MenuButton, MenuItem } from './menu'
import { Provider } from './provider'
import { Rail } from './rail'
import { Root } from './root'
import { Separator } from './separator'
import { MenuSub, MenuSubButton, MenuSubItem } from './subMenu'
import { Trigger } from './trigger'
 
const Sidebar = {
  Provider,
  Root,
  Content,
  Inset,
  Header,
  Footer,
  Group,
  GroupAction,
  GroupContent,
  GroupLabel,
  Menu,
  MenuAction,
  MenuBadge,
  MenuButton,
  MenuItem,
  MenuSub,
  MenuSubButton,
  MenuSubItem,
  Rail,
  Separator,
  Trigger,
  useSidebar,
}
 
export default Sidebar

Update the import paths to match your project setup

Usage

import Sidebar from '@/components/ui/sidebar'
<Sidebar.Provider>
    <Sidebar.Root>
        <Sidebar.Header></Sidebar.Header>
        <Sidebar.Content>
        <Sidebar.Group>
            <Sidebar.GroupLabel></Sidebar.GroupLabel>
            <Sidebar.GroupAction></Sidebar.GroupAction>
            <Sidebar.GroupContent>
            <Sidebar.Menu>
                <Sidebar.MenuItem>
                <Sidebar.MenuButton></Sidebar.MenuButton>
                </Sidebar.MenuItem>
                <Sidebar.MenuItem>
                <Sidebar.MenuButton></Sidebar.MenuButton>
                </Sidebar.MenuItem>
                <Sidebar.MenuItem>
                <Sidebar.MenuButton></Sidebar.MenuButton>
                <Sidebar.MenuSub>
                    <Sidebar.MenuSubItem>
                    <Sidebar.MenuSubButton></Sidebar.MenuSubButton>
                    </Sidebar.MenuSubItem>
                    <Sidebar.MenuSubItem>
                    <Sidebar.MenuSubButton></Sidebar.MenuSubButton>
                    </Sidebar.MenuSubItem>
                    <Sidebar.MenuSubItem>
                    <Sidebar.MenuSubButton></Sidebar.MenuSubButton>
                    </Sidebar.MenuSubItem>
                </Sidebar.MenuSub>
                </Sidebar.MenuItem>
            </Sidebar.Menu>
            </Sidebar.GroupContent>
        </Sidebar.Group>
        </Sidebar.Content>
        <Sidebar.Footer></Sidebar.Footer>
    </Sidebar.Root>
</Sidebar.Provider>

Examples

Default

  • Acme IncEnterprise
Application
  • 12
    • Users
    • Groups
    • Media
  • Pallas UICarbonteq

Inset

  • Acme IncEnterprise
Application
  • 12
    • Users
    • Groups
    • Media
  • Pallas UICarbonteq

Floating

  • Acme IncEnterprise
Application
  • 12
    • Users
    • Groups
    • Media
  • Pallas UICarbonteq

Icons

  • Acme IncEnterprise
Application
  • 12
    • Users
    • Groups
    • Media
  • Pallas UICarbonteq

API Reference

Sidebar.Provider

Wraps all sidebar parts and provides shared state (open/closed, mobile mode, toggling) via context.

Props

PropertyTypeDefaultDescriptionOptions
defaultOpenbooleantrueInitial open state (uncontrolled).true, false
openboolean-Controlled open state.true, false
onOpenChange(open: boolean) => void-Callback fired when open state changes (controlled mode).-

Sidebar.Root

The root element

Sidebar.Inset

The wrapper element for sidebar siblings in 'inset' variant

Sidebar.Content

The content wrapper element

Sidebar.Header

The header wrapper element

Sidebar.Footer

The footer wrapper element

Sidebar.Group

The group wrapper element

Sidebar.GroupLabel

The group label element

Props

PropertyTypeDefaultDescriptionOptions
asChildbooleanfalseRender as a child component instead of a divtrue, false

Sidebar.GroupAction

The group action element

Props

PropertyTypeDefaultDescriptionOptions
asChildbooleanfalseRender as a child component instead of a buttontrue, false

Sidebar.GroupContent

The group content wrapper element

Sidebar.Menu

The menu wrapper element

Sidebar.MenuItem

The menu item element

Sidebar.MenuButton

The menu button element

Props

PropertyTypeDefaultDescriptionOptions
asChildbooleanfalseRender as child instead of buttontrue, false
isActivebooleanfalseIndicates if the menu button is activetrue, false

Data attributes

AttributeValues
data-activetrue, false

Sidebar.MenuAction

The menu action element

Props

PropertyTypeDefaultDescriptionOptions
asChildbooleanfalseRender as child instead of buttontrue, false
showOnHoverbooleanfalseShow action only on hovertrue, false

Data attributes

AttributeValues
data-showOnHovertrue, false

Sidebar.MenuBadge

The menu badge element

Sidebar.MenuSub

The sub menu wrapper element

Sidebar.MenuSubItem

The sub menu item element

Sidebar.MenuSubButton

The sub menu button element

Props

PropertyTypeDefaultDescriptionOptions
asChildbooleanfalseRender as child instead of anchortrue, false
size'sm' | 'md' | 'lg'mdButton sizesm, md, lg
isActiveboolean-If the sub menu button is activetrue, false

Data attributes

AttributeValues
data-sizesm, md, lg
data-activetrue, false

Sidebar.Trigger

The trigger element for toggle

Props

PropertyTypeDefaultDescriptionOptions
onClick(event: React.MouseEvent) => void-Click handler before toggling sidebar-

Sidebar.Rail

The rail element for toggle

Sidebar.useSidebar

The hook to control toggle manually

Built with ❤️ by the carbonteq team. The source code is available on GitHub.

© 2025 Pallas UI. All rights reserved.