Sidebar

A composable, themeable and customizable sidebar component.

Structure

A Sidebar component is composed of the following parts:

  • SidebarProvider - Handles collapsible state.
  • Sidebar - The sidebar container.
  • SidebarHeader and SidebarFooter - Sticky at the top and bottom of the sidebar
  • SidebarContent - Scrollable content.
  • SidebarGroup - Section within the SidebarContent.
  • SidebarTrigger - Trigger for the Sidebar

Sidebar Structure

Usage

App.vue
[AppSidebar.vue] via components
[AppSidebar.vue] via slots
<template>
  <NSidebarProvider>
    <AppSidebar />

    <main>
      <NSidebarTrigger />
      <NuxtPage />
    </main>
  </NSidebarProvider>
</template>

Examples

Basic

PropDefaultTypeDescription
sheetleftleft,rightThe side of the sheet. Options are left and right.
sidebarsidebarsidebar,floating,insetThe variant of the sidebar.
collapsibleoffcanvasoffcanvas,icon,noneCollapsible behavior.
railtruebooleanWhether to display the sidebar rail for resizing.
Preview
Dashboard.vue
AppSidebar.vue
TeamSwitcher.vue
NavMain.vue
NavProjects.vue
NavUser.vue

A collapsible nested sidebar

Preview
Dashboard.vue
AppSidebar.vue
NavUser.vue

Customization

You can customize the sidebar using the following sub components props and una prop.

NameTypeDescription
_sidebarContentNSidebarContentPropsProps for the content component.
_sidebarHeaderNSidebarHeaderPropsProps for the header component.
_sidebarFooterNSidebarFooterPropsProps for the footer component.
_sidebarRailNSidebarRailPropsProps for the rail component.
unaNSidebarUnaPropsUnaUI preset configuration for all sidebar components.

Slots

NamePropsDescription
default-The main content of the sidebar, includes the header, content and footer.
header-The header content, appears at the top.
content-The main content of the sidebar.
footer-The footer content, appears at the bottom.

Composables

useSidebar()

The useSidebar composable provides reactive access to the sidebar's state.

Usage.vue
<script setup lang="ts">
const {
  isMobile, // Whether the sidebar is in mobile view
  state, // Current state: 'open', 'closed', or 'collapsed'
  openMobile, // Whether the mobile sidebar is open
  setOpenMobile // Function to set the mobile sidebar open state
} = useSidebar()
</script>

<template>
  <div>
    <button @click="setOpenMobile(true)">
      Open Mobile
    </button>
  </div>
</template>
composables/useSidebar.ts
import type { ComputedRef, Ref } from 'vue'
import { createContext } from 'reka-ui'

export const SIDEBAR_COOKIE_NAME = 'sidebar:state'
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
export const SIDEBAR_WIDTH = '16rem'
export const SIDEBAR_WIDTH_MOBILE = '18rem'
export const SIDEBAR_WIDTH_ICON = '3rem'
export const SIDEBAR_KEYBOARD_SHORTCUT = 'b'

export const [useSidebar, provideSidebarContext] = createContext<{
  state: ComputedRef<'expanded' | 'collapsed'>
  open: Ref<boolean>
  setOpen: (value: boolean) => void
  isMobile: Ref<boolean>
  openMobile: Ref<boolean>
  setOpenMobile: (value: boolean) => void
  toggleSidebar: () => void
}>('Sidebar')

Props

types/sidebar.ts
import type { PrimitiveProps } from 'reka-ui'
import type { Component, HTMLAttributes } from 'vue'
import type { NButtonProps, NInputProps } from '.'
import type { NSheetContentProps } from './sheet'

/**
 * Sidebar component props interface
 */
export interface NSidebarProps {
  /**
   * The side of the sidebar.
   *
   * @default 'left'
   */
  sheet?: 'left' | 'right'

  /**
   * The variant of the sidebar.
   *
   * @default 'sidebar'
   */
  sidebar?: 'sidebar' | 'floating' | 'inset'

  /**
   * Collapsible behavior.
   *
   * @default 'offcanvas'
   */
  collapsible?: 'offcanvas' | 'icon' | 'none'

  /**
   * Additional classes to apply to the sidebar.
   */
  class?: HTMLAttributes['class']

  /**
   * Whether to display the sidebar rail for resizing.
   *
   * @default true
   */
  rail?: boolean

  /**
   * Props passed to the sheet content component when in mobile view.
   */
  _sheetContent?: NSheetContentProps

  // Sub components
  _sidebarContent?: NSidebarContentProps
  _sidebarHeader?: NSidebarHeaderProps
  _sidebarFooter?: NSidebarFooterProps
  _sidebarRail?: NSidebarRailProps

  /**
   * `UnaUI` preset configuration
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/sidebar.ts
   */
  una?: NSidebarUnaProps
}

/**
 * Sidebar provider component props interface
 */
export interface NSidebarProviderProps {
  /**
   * Default open state.
   *
   * @default true
   */
  defaultOpen?: boolean

  /**
   * Controlled open state.
   */
  open?: boolean

  /**
   * Additional classes to apply to the provider.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarProvider'>
}

/**
 * Sidebar content component props interface
 */
export interface NSidebarContentProps {
  /**
   * Additional attributes that can be passed to the sidebar content element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the content.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarContent'>
}

/**
 * Sidebar header component props interface
 */
export interface NSidebarHeaderProps {
  /**
   * Additional attributes that can be passed to the sidebar header element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the header.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarHeader'>
}

/**
 * Sidebar footer component props interface
 */
export interface NSidebarFooterProps {
  /**
   * Additional attributes that can be passed to the sidebar footer element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the footer.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarFooter'>
}

/**
 * Sidebar group component props interface
 */
export interface NSidebarGroupProps {
  /**
   * The label of the group.
   */
  label?: string

  /**
   * Additional attributes that can be passed to the sidebar group element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the group.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarGroup'>
}

/**
 * Sidebar group content component props interface
 */
export interface NSidebarGroupContentProps {
  /**
   * Additional attributes that can be passed to the sidebar group content element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the group content.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarGroupContent'>
}

/**
 * Sidebar group label component props interface
 */
export interface NSidebarGroupLabelProps extends PrimitiveProps {
  /**
   * Additional attributes that can be passed to the sidebar group label element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the group label.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarGroupLabel'>
}

/**
 * Sidebar group action component props interface
 */
export interface NSidebarGroupActionProps extends PrimitiveProps {
  /**
   * Additional attributes that can be passed to the sidebar group action element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the group action.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarGroupAction'>
}

/**
 * Sidebar menu component props interface
 */
export interface NSidebarMenuProps<T extends { id?: string | number } | Record<string, any> = any> {
  /**
   * Array of items to render in the menu.
   */
  items?: T[]

  /**
   * Property from each item to use as a key.
   *
   * @default 'id'
   */
  itemKey?: keyof T

  /**
   * Additional attributes that can be passed to the sidebar menu element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the menu.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarMenu'>
}

/**
 * Sidebar menu item component props interface
 */
export interface NSidebarMenuItemProps {
  /**
   * Additional attributes that can be passed to the sidebar menu item element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the menu item.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarMenuItem'>
}

/**
 * Sidebar menu button child component props interface
 */
export interface NSidebarMenuButtonChildProps extends PrimitiveProps {
  /**
   * The variant of the button.
   *
   * @default 'default'
   */
  variant?: 'default' | 'outline'

  /**
   * The size of the button.
   *
   * @default 'default'
   */
  size?: 'default' | 'sm' | 'lg'

  /**
   * Whether the button is in active state.
   *
   * @default false
   */
  isActive?: boolean

  /**
   * Additional classes to apply to the button.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarMenuButtonChild'>
}

/**
 * Sidebar menu button component props interface
 */
export interface NSidebarMenuButtonProps extends Omit<NSidebarMenuButtonChildProps, 'una'> {
  /**
   * Tooltip content to show when sidebar is collapsed.
   */
  tooltip?: string | Component

  /**
   * Whether the button is in active state.
   *
   * @default false
   */
  isActive?: boolean

  /**
   * The variant of the button.
   *
   * @default 'default'
   */
  variant?: 'default' | 'outline'

  /**
   * The size of the button.
   *
   * @default 'default'
   */
  size?: 'default' | 'sm' | 'lg'

  /**
   * Additional classes to apply to the button.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarMenuButton' | 'sidebarMenuButtonChild'>
}

/**
 * Sidebar menu sub component props interface
 */
export interface NSidebarMenuSubProps {
  /**
   * Additional attributes that can be passed to the sidebar menu sub element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the menu sub.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarMenuSub'>
}

/**
 * Sidebar menu sub item component props interface
 */
export interface NSidebarMenuSubItemProps {
  /**
   * Additional attributes that can be passed to the sidebar menu sub item element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the menu sub item.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarMenuSubItem'>
}

/**
 * Sidebar menu sub button component props interface
 */
export interface NSidebarMenuSubButtonProps extends PrimitiveProps {
  /**
   * Whether the button is in active state.
   *
   * @default false
   */
  isActive?: boolean

  /**
   * The size of the button.
   *
   * @default 'md'
   */
  size?: 'sm' | 'md'

  /**
   * Additional classes to apply to the button.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarMenuSubButton'>
}

/**
 * Sidebar menu action component props interface
 */
export interface NSidebarMenuActionProps extends PrimitiveProps {
  /**
   * Whether to show the action only on hover.
   *
   * @default false
   */
  showOnHover?: boolean

  /**
   * Additional attributes that can be passed to the sidebar menu action element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the menu action.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarMenuAction'>
}

/**
 * Sidebar menu badge component props interface
 */
export interface NSidebarMenuBadgeProps {
  /**
   * Additional attributes that can be passed to the sidebar menu badge element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the menu badge.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarMenuBadge'>
}

/**
 * Sidebar menu skeleton component props interface
 */
export interface NSidebarMenuSkeletonProps {
  /**
   * Whether to show the icon in the skeleton.
   *
   * @default false
   */
  showIcon?: boolean

  /**
   * Additional attributes that can be passed to the sidebar menu skeleton element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the menu skeleton.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarMenuSkeleton'>
}

/**
 * Sidebar separator component props interface
 */
export interface NSidebarSeparatorProps {
  /**
   * Additional attributes that can be passed to the sidebar separator element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the separator.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarSeparator'>
}

/**
 * Sidebar rail component props interface
 */
export interface NSidebarRailProps {
  /**
   * Additional attributes that can be passed to the sidebar rail element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the rail.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarRail'>
}

/**
 * Sidebar inset component props interface
 */
export interface NSidebarInsetProps {
  /**
   * Additional attributes that can be passed to the sidebar inset element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the inset.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarInset'>
}

/**
 * Sidebar input component props interface
 */
export interface NSidebarInputProps extends NInputProps {
  /**
   * Additional attributes that can be passed to the sidebar input element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the input.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarInput'> & NInputProps['una']
}

/**
 * Sidebar trigger component props interface
 */
export interface NSidebarTriggerProps extends Omit<NButtonProps, 'una'> {
  /**
   * Additional attributes that can be passed to the sidebar trigger element.
   */
  [key: string]: any

  /**
   * Additional classes to apply to the trigger.
   */
  class?: HTMLAttributes['class']

  /**
   * `UnaUI` preset configuration
   */
  una?: Pick<NSidebarUnaProps, 'sidebarTrigger'> & NButtonProps['una']
}

/**
 * UnaUI preset configuration for sidebar components
 */
export interface NSidebarUnaProps {
  sidebar?: HTMLAttributes['class']
  sidebarProvider?: HTMLAttributes['class']
  sidebarContent?: HTMLAttributes['class']
  sidebarHeader?: HTMLAttributes['class']
  sidebarFooter?: HTMLAttributes['class']
  sidebarGroup?: HTMLAttributes['class']
  sidebarGroupContent?: HTMLAttributes['class']
  sidebarGroupLabel?: HTMLAttributes['class']
  sidebarGroupAction?: HTMLAttributes['class']
  sidebarMenu?: HTMLAttributes['class']
  sidebarMenuItem?: HTMLAttributes['class']
  sidebarMenuButton?: HTMLAttributes['class']
  sidebarMenuButtonChild?: HTMLAttributes['class']
  sidebarMenuSub?: HTMLAttributes['class']
  sidebarMenuSubItem?: HTMLAttributes['class']
  sidebarMenuSubButton?: HTMLAttributes['class']
  sidebarMenuAction?: HTMLAttributes['class']
  sidebarMenuBadge?: HTMLAttributes['class']
  sidebarMenuSkeleton?: HTMLAttributes['class']
  sidebarSeparator?: HTMLAttributes['class']
  sidebarRail?: HTMLAttributes['class']
  sidebarInset?: HTMLAttributes['class']
  sidebarInput?: HTMLAttributes['class']
  sidebarTrigger?: HTMLAttributes['class']
}

Presets

shortcuts/sidebar.ts
type SidebarPrefix = 'sidebar'

export const staticSidebar: Record<`${SidebarPrefix}-${string}` | SidebarPrefix, string> = {
  // base
  'sidebar': '',
  // mobile (sheet)
  'sidebar-mobile': 'w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden',
  'sidebar-mobile-inner': 'h-full w-full flex flex-col',
  // collapsible variants
  'sidebar-collapsible-none': 'flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground',
  // desktop container
  'sidebar-desktop': 'hidden md:block',
  'sidebar-desktop-inner': 'h-full w-full flex flex-col bg-sidebar text-sidebar-foreground group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow',
  // gap handling
  'sidebar-desktop-gap': 'duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear group-data-[collapsible=offcanvas]:w-0 group-data-[side=right]:rotate-180',
  'sidebar-desktop-gap-floating': 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_1rem)]',
  'sidebar-desktop-gap-default': 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]',
  // positioning
  'sidebar-desktop-position': 'duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex',
  'sidebar-desktop-position-left': 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]',
  'sidebar-desktop-position-right': 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
  // padding variations
  'sidebar-desktop-padding-floating': 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_1rem_+_2px)]',
  'sidebar-desktop-padding-default': 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l',

  // subcomponents
  'sidebar-provider': 'flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar',
  'sidebar-content': 'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
  'sidebar-header': 'flex flex-col gap-2 p-2',
  'sidebar-group': 'relative flex w-full min-w-0 flex-col p-2',
  'sidebar-group-action': 'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 after:absolute after:-inset-2 after:md:hidden group-data-[collapsible=icon]:hidden',
  'sidebar-group-content': 'w-full text-sm',
  'sidebar-group-label': 'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] ease-linear focus-visible:ring-2 [&>span[icon-base]]:square-4 [&>svg]:shrink-0',
  'sidebar-input': 'h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring',
  'sidebar-inset': 'relative flex min-h-svh flex-1 flex-col bg-background data-[variant=inset]:min-h-[calc(100svh-1rem)] md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:data-[state=collapsed]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
  'sidebar-menu': 'flex w-full min-w-0 flex-col gap-1',
  'sidebar-menu-action': 'absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none 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 after:absolute after:-inset-2 after:md: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',
  'sidebar-menu-action-show-on-hover': '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',
  'sidebar-menu-skeleton': 'rounded-md h-8 flex gap-2 px-2 items-center',
  'sidebar-separator': 'mx-2 w-auto bg-sidebar-border',
  'sidebar-menu-item': 'relative',
  'sidebar-menu-sub': 'mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden',
  'sidebar-menu-sub-item': '',
  'sidebar-rail': '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 [[data-side=left]_&]:cursor-w-resize [[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 group-data-[collapsible=offcanvas]:hover:bg-sidebar [[data-side=left][data-collapsible=offcanvas]_&]:-right-2 [[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
  'sidebar-menu-badge': 'absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none 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',
  'sidebar-footer': 'flex flex-col gap-2 p-2',

  // TODO: Add these to the preset
  'sidebar-menu-button': '',
  'sidebar-menu-button-child': '',
  'sidebar-menu-sub-button': '',
}

export const dynamicSidebar: [RegExp, (params: RegExpExecArray) => string][] = [
  // dynamic preset
]

export const sidebar = [
  ...dynamicSidebar,
  staticSidebar,
]

Components

SidebarProvider.vue
Sidebar.vue
SidebarContent.vue
SidebarHeader.vue
SidebarFooter.vue
SidebarMenu.vue
SidebarMenuItem.vue
SidebarMenuButton.vue
SidebarMenuButtonChild.vue
SidebarMenuSubButton.vue
SidebarMenuSub.vue
SidebarMenuSubItem.vue
SidebarMenuAction.vue
SidebarMenuBadge.vue
SidebarGroup.vue
SidebarGroupLabel.vue
SidebarGroupContent.vue
SidebarRail.vue
<script setup lang="ts">
import type { Ref } from 'vue'
import type { NSidebarProviderProps } from '../../types'
import { useEventListener, useMediaQuery, useVModel } from '@vueuse/core'
import { TooltipProvider } from 'reka-ui'
import { computed, ref } from 'vue'
import { provideSidebarContext, SIDEBAR_COOKIE_MAX_AGE, SIDEBAR_COOKIE_NAME, SIDEBAR_KEYBOARD_SHORTCUT, SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON } from '../../composables/useSidebar'
import { cn } from '../../utils'

const props = withDefaults(defineProps<NSidebarProviderProps>(), {
  defaultOpen: true,
  open: undefined,
})

const emits = defineEmits<{
  'update:open': [open: boolean]
}>()

const isMobile = useMediaQuery('(max-width: 768px)')
const openMobile = ref(false)

const open = useVModel(props, 'open', emits, {
  defaultValue: props.defaultOpen ?? false,
  passive: (props.open === undefined) as false,
}) as Ref<boolean>

function setOpen(value: boolean) {
  open.value = value // emits('update:open', value)

  // This sets the cookie to keep the sidebar state.
  document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
}

function setOpenMobile(value: boolean) {
  openMobile.value = value
}

// Helper to toggle the sidebar.
function toggleSidebar() {
  return isMobile.value ? setOpenMobile(!openMobile.value) : setOpen(!open.value)
}

useEventListener('keydown', (event: KeyboardEvent) => {
  if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
    event.preventDefault()
    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 = computed(() => open.value ? 'expanded' : 'collapsed')

provideSidebarContext({
  state,
  open,
  setOpen,
  isMobile,
  openMobile,
  setOpenMobile,
  toggleSidebar,
})
</script>

<template>
  <TooltipProvider :delay-duration="0">
    <div
      :style="{
        '--sidebar-width': SIDEBAR_WIDTH,
        '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
      }"
      :class="cn(
        'group/sidebar_wrapper sidebar-provider',
        props.una?.sidebarProvider,
        props.class,
      )"
      v-bind="$attrs"
    >
      <slot />
    </div>
  </TooltipProvider>
</template>