A flexible and accessible sidebar navigation component built with Radix UI primitives.
npm install @radix-ui/react-slotimport { 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>
)
})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'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')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')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')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')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')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')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')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')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} />
}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''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 Sidebarimport 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>Wraps all sidebar parts and provides shared state (open/closed, mobile mode, toggling) via context.
| Property | Type | Default | Description | Options |
|---|---|---|---|---|
defaultOpen | boolean | true | Initial open state (uncontrolled). | true, false |
open | boolean | - | Controlled open state. | true, false |
onOpenChange | (open: boolean) => void | - | Callback fired when open state changes (controlled mode). | - |
The root element
The wrapper element for sidebar siblings in 'inset' variant
The content wrapper element
The header wrapper element
The footer wrapper element
The group wrapper element
The group label element
| Property | Type | Default | Description | Options |
|---|---|---|---|---|
| asChild | boolean | false | Render as a child component instead of a div | true, false |
The group action element
| Property | Type | Default | Description | Options |
|---|---|---|---|---|
| asChild | boolean | false | Render as a child component instead of a button | true, false |
The group content wrapper element
The menu wrapper element
The menu item element
The menu button element
| Property | Type | Default | Description | Options |
|---|---|---|---|---|
| asChild | boolean | false | Render as child instead of button | true, false |
| isActive | boolean | false | Indicates if the menu button is active | true, false |
| Attribute | Values |
|---|---|
| data-active | true, false |
The menu action element
| Property | Type | Default | Description | Options |
|---|---|---|---|---|
| asChild | boolean | false | Render as child instead of button | true, false |
| showOnHover | boolean | false | Show action only on hover | true, false |
| Attribute | Values |
|---|---|
| data-showOnHover | true, false |
The menu badge element
The sub menu wrapper element
The sub menu item element
The sub menu button element
| Property | Type | Default | Description | Options |
|---|---|---|---|---|
| asChild | boolean | false | Render as child instead of anchor | true, false |
| size | 'sm' | 'md' | 'lg' | md | Button size | sm, md, lg |
| isActive | boolean | - | If the sub menu button is active | true, false |
| Attribute | Values |
|---|---|
| data-size | sm, md, lg |
| data-active | true, false |
The trigger element for toggle
| Property | Type | Default | Description | Options |
|---|---|---|---|---|
| onClick | (event: React.MouseEvent) => void | - | Click handler before toggling sidebar | - |
The rail element for toggle
The hook to control toggle manually