Sidebar
Vue navigationFull-height app sidebar — collapsible to icons, with grouping, sub-grouping, and integrated search. The navigation surface for product apps with many sections.
Also available for React ->Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/sidebar.json$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/sidebar.json$ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/sidebar.json$ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/sidebar.json
Or with the named registry:
npx shadcn-vue@latest add @uipkge/sidebar
Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
variant | 'default''outline' | default | optional |
size | 'default''sm''lg' | default | optional |
Dependencies
Used by
Files (27)
-
app/components/ui/sidebar/Sidebar.vue 3.9 kB
<script setup lang="ts"> import type { SidebarProps } from '.' import { cn } from '@/lib/utils' import { Sheet, SheetContent } from '@/components/ui/sheet' import SheetDescription from '@/components/ui/sheet/SheetDescription.vue' import SheetHeader from '@/components/ui/sheet/SheetHeader.vue' import SheetTitle from '@/components/ui/sheet/SheetTitle.vue' import { SIDEBAR_WIDTH_MOBILE, useSidebar } from './utils' defineOptions({ inheritAttrs: false, }) const props = withDefaults(defineProps<SidebarProps>(), { side: 'left', variant: 'sidebar', collapsible: 'offcanvas', }) const { isMobile, state, openMobile, setOpenMobile } = useSidebar() </script> <template> <div v-if="collapsible === 'none'" data-uipkge data-slot="sidebar" :class="cn('bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col', props.class)" v-bind="$attrs" > <slot /> </div> <Sheet v-else-if="isMobile" :open="openMobile" v-bind="$attrs" @update:open="setOpenMobile"> <SheetContent data-sidebar="sidebar" data-uipkge data-slot="sidebar" data-mobile="true" :side="side" class="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden" :style="{ '--sidebar-width': SIDEBAR_WIDTH_MOBILE, }" > <SheetHeader class="sr-only"> <SheetTitle>Sidebar</SheetTitle> <SheetDescription>Displays the mobile sidebar.</SheetDescription> </SheetHeader> <div class="flex h-full w-full flex-col"> <slot /> </div> </SheetContent> </Sheet> <div v-else class="group peer text-sidebar-foreground hidden md:block" data-uipkge data-slot="sidebar" :data-state="state" :data-collapsible="state === 'collapsed' ? collapsible : ''" :data-variant="variant" :data-side="side" > <!-- This is what handles the sidebar gap on desktop --> <div :class=" 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)', ) " /> <div :class=" 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', props.class, ) " v-bind="$attrs" > <div data-sidebar="sidebar" class="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm" > <slot /> </div> </div> </div> </template> <style> /* Push any OverlayScroll thumb inside a sidebar 10px in from the right edge so it clears SidebarRail's 8px inside-zone. The CSS variable cascades to every descendant, so consumers wrapping their nav in OverlayScroll (a common pattern when nav items overflow viewport height) get this for free -- no per-call thumb-offset config. */ [data-slot='sidebar'] { --ovs-thumb-right: 10px; } </style> -
app/components/ui/sidebar/SidebarContent.vue 0.4 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' const props = defineProps<{ class?: HTMLAttributes['class'] }>() </script> <template> <div data-uipkge data-slot="sidebar-content" data-sidebar="content" :class=" cn('flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden', props.class) " > <slot /> </div> </template> -
app/components/ui/sidebar/SidebarFooter.vue 0.3 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' const props = defineProps<{ class?: HTMLAttributes['class'] }>() </script> <template> <div data-uipkge data-slot="sidebar-footer" data-sidebar="footer" :class="cn('flex flex-col gap-2 p-2', props.class)"> <slot /> </div> </template> -
app/components/ui/sidebar/SidebarGroup.vue 0.4 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' const props = defineProps<{ class?: HTMLAttributes['class'] }>() </script> <template> <div data-uipkge data-slot="sidebar-group" data-sidebar="group" :class="cn('relative flex w-full min-w-0 flex-col p-2', props.class)" > <slot /> </div> </template> -
app/components/ui/sidebar/SidebarGroupAction.vue 0.9 kB
<script setup lang="ts"> import type { PrimitiveProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { Primitive } from 'reka-ui' import { cn } from '@/lib/utils' const props = defineProps< PrimitiveProps & { class?: HTMLAttributes['class'] } >() </script> <template> <Primitive data-uipkge data-slot="sidebar-group-action" data-sidebar="group-action" :as="as" :as-child="asChild" :class=" cn( 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', 'after:absolute after:-inset-2 md:after:hidden', 'group-data-[collapsible=icon]:hidden', props.class, ) " > <slot /> </Primitive> </template> -
app/components/ui/sidebar/SidebarGroupContent.vue 0.4 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' const props = defineProps<{ class?: HTMLAttributes['class'] }>() </script> <template> <div data-uipkge data-slot="sidebar-group-content" data-sidebar="group-content" :class="cn('w-full text-sm', props.class)" > <slot /> </div> </template> -
app/components/ui/sidebar/SidebarGroupLabel.vue 0.9 kB
<script setup lang="ts"> import type { PrimitiveProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { Primitive } from 'reka-ui' import { cn } from '@/lib/utils' const props = defineProps< PrimitiveProps & { class?: HTMLAttributes['class'] } >() </script> <template> <Primitive data-uipkge data-slot="sidebar-group-label" data-sidebar="group-label" :as="as" :as-child="asChild" :class=" cn( 'text-sidebar-foreground/70 ring-sidebar-ring mt-2 mb-1 flex h-8 shrink-0 items-center rounded-md px-2 text-[13px] font-medium outline-hidden 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', props.class, ) " > <slot /> </Primitive> </template> -
app/components/ui/sidebar/SidebarHeader.vue 0.3 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' const props = defineProps<{ class?: HTMLAttributes['class'] }>() </script> <template> <div data-uipkge data-slot="sidebar-header" data-sidebar="header" :class="cn('flex flex-col gap-2 p-2', props.class)"> <slot /> </div> </template> -
app/components/ui/sidebar/SidebarInput.vue 0.7 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' import { Input } from '@/components/ui/input' const props = defineProps<{ class?: HTMLAttributes['class'] }>() </script> <template> <!-- Sidebar-tuned wrapper over the kit's <Input>. Forwards every slot so consumers can pass prefix (search icon) / suffix (kbd hint) / default slot content without losing the sidebar height + bg defaults. --> <Input data-uipkge data-slot="sidebar-input" data-sidebar="input" :class="cn('bg-background h-8 w-full shadow-none', props.class)" > <template v-for="(_, name) in $slots" #[name]="slotProps"> <slot :name="name" v-bind="slotProps" /> </template> </Input> </template> -
app/components/ui/sidebar/SidebarInset.vue 0.9 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' const props = defineProps<{ class?: HTMLAttributes['class'] }>() </script> <template> <main data-uipkge data-slot="sidebar-inset" :class=" cn( // min-w-0 is load-bearing: a flex-1 child without it inherits min-width: auto, // which means any single wide descendant (chart, table, code block) blows the // inset's width past its grid track. The upstream shadcn-ui sidebar omits this. 'bg-background relative flex w-full min-w-0 flex-1 flex-col', '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 md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2', props.class, ) " > <slot /> </main> </template> -
app/components/ui/sidebar/SidebarMenu.vue 0.4 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' const props = defineProps<{ class?: HTMLAttributes['class'] }>() </script> <template> <ul data-uipkge data-slot="sidebar-menu" data-sidebar="menu" :class="cn('flex w-full min-w-0 flex-col gap-1', props.class)" > <slot /> </ul> </template> -
app/components/ui/sidebar/SidebarMenuAction.vue 1.4 kB
<script setup lang="ts"> import type { PrimitiveProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { Primitive } from 'reka-ui' import { cn } from '@/lib/utils' const props = withDefaults( defineProps< PrimitiveProps & { showOnHover?: boolean class?: HTMLAttributes['class'] } >(), { as: 'button', }, ) </script> <template> <Primitive data-uipkge data-slot="sidebar-menu-action" data-sidebar="menu-action" :class=" cn( 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', '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 && 'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0', props.class, ) " :as="as" :as-child="asChild" > <slot /> </Primitive> </template> -
app/components/ui/sidebar/SidebarMenuBadge.vue 0.9 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' const props = defineProps<{ class?: HTMLAttributes['class'] }>() </script> <template> <div data-uipkge data-slot="sidebar-menu-badge" data-sidebar="menu-badge" :class=" cn( 'text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-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', props.class, ) " > <slot /> </div> </template> -
app/components/ui/sidebar/SidebarMenuButton.vue 1.3 kB
<script setup lang="ts"> import type { Component } from 'vue' import type { SidebarMenuButtonProps } from './SidebarMenuButtonChild.vue' import { reactiveOmit } from '@vueuse/core' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import SidebarMenuButtonChild from './SidebarMenuButtonChild.vue' import { useSidebar } from './utils' defineOptions({ inheritAttrs: false, }) const props = withDefaults( defineProps< SidebarMenuButtonProps & { tooltip?: string | Component } >(), { as: 'button', variant: 'default', size: 'default', }, ) const { isMobile, state } = useSidebar() const delegatedProps = reactiveOmit(props, 'tooltip') </script> <template> <SidebarMenuButtonChild v-if="!tooltip" v-bind="{ ...delegatedProps, ...$attrs }"> <slot /> </SidebarMenuButtonChild> <Tooltip v-else> <TooltipTrigger as-child> <SidebarMenuButtonChild v-bind="{ ...delegatedProps, ...$attrs }"> <slot /> </SidebarMenuButtonChild> </TooltipTrigger> <TooltipContent side="right" align="center" :hidden="state !== 'collapsed' || isMobile"> <template v-if="typeof tooltip === 'string'"> {{ tooltip }} </template> <component :is="tooltip" v-else /> </TooltipContent> </Tooltip> </template> -
app/components/ui/sidebar/SidebarMenuButtonChild.vue 1.3 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { Primitive } from 'reka-ui' import { cn } from '@/lib/utils' import { sidebarMenuButtonVariants } from './sidebar.variants' // Inline `as`/`asChild` from PrimitiveProps -- see Button.vue for why // `extends /* @vue-ignore */ PrimitiveProps` was wrong: the annotation // drops every interface field from runtime props, not just the // extended ones, so variant/size/isActive silently become attrs. // // Same goes for variant/size: the SFC compiler can't extract them // from `SidebarMenuButtonVariants['variant']` indexed-access types, // so the unions are inlined explicitly. export interface SidebarMenuButtonProps { as?: string asChild?: boolean variant?: 'default' | 'outline' size?: 'default' | 'sm' | 'lg' isActive?: boolean class?: HTMLAttributes['class'] } const props = withDefaults(defineProps<SidebarMenuButtonProps>(), { as: 'button', variant: 'default', size: 'default', }) </script> <template> <Primitive data-uipkge data-slot="sidebar-menu-button" data-sidebar="menu-button" :data-size="size" :data-active="isActive" :class="cn(sidebarMenuButtonVariants({ variant, size }), props.class)" :as="as" :as-child="asChild" v-bind="$attrs" > <slot /> </Primitive> </template> -
app/components/ui/sidebar/SidebarMenuItem.vue 0.4 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' const props = defineProps<{ class?: HTMLAttributes['class'] }>() </script> <template> <li data-uipkge data-slot="sidebar-menu-item" data-sidebar="menu-item" :class="cn('group/menu-item relative', props.class)" > <slot /> </li> </template> -
app/components/ui/sidebar/SidebarMenuSkeleton.vue 0.8 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { computed } from 'vue' import { cn } from '@/lib/utils' import { Skeleton } from '@/components/ui/skeleton' const props = defineProps<{ showIcon?: boolean class?: HTMLAttributes['class'] }>() const width = computed(() => { return `${Math.floor(Math.random() * 40) + 50}%` }) </script> <template> <div data-uipkge data-slot="sidebar-menu-skeleton" data-sidebar="menu-skeleton" :class="cn('flex h-8 items-center gap-2 rounded-md px-2', props.class)" > <Skeleton v-if="showIcon" class="size-4 rounded-md" data-sidebar="menu-skeleton-icon" /> <Skeleton class="h-4 max-w-(--skeleton-width) flex-1" data-sidebar="menu-skeleton-text" :style="{ '--skeleton-width': width }" /> </div> </template> -
app/components/ui/sidebar/SidebarMenuSub.vue 0.5 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' const props = defineProps<{ class?: HTMLAttributes['class'] }>() </script> <template> <ul data-uipkge data-slot="sidebar-menu-sub" data-sidebar="menu-badge" :class=" cn( 'border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5', 'group-data-[collapsible=icon]:hidden', props.class, ) " > <slot /> </ul> </template> -
app/components/ui/sidebar/SidebarMenuSubButton.vue 1.4 kB
<script setup lang="ts"> import type { PrimitiveProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { Primitive } from 'reka-ui' import { cn } from '@/lib/utils' const props = withDefaults( defineProps< PrimitiveProps & { size?: 'sm' | 'md' isActive?: boolean class?: HTMLAttributes['class'] } >(), { as: 'a', size: 'md', }, ) </script> <template> <Primitive data-uipkge data-slot="sidebar-menu-sub-button" data-sidebar="menu-sub-button" :as="as" :as-child="asChild" :data-size="size" :data-active="isActive" :class=" cn( 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 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', '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', props.class, ) " > <slot /> </Primitive> </template> -
app/components/ui/sidebar/SidebarMenuSubItem.vue 0.4 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' const props = defineProps<{ class?: HTMLAttributes['class'] }>() </script> <template> <li data-uipkge data-slot="sidebar-menu-sub-item" data-sidebar="menu-sub-item" :class="cn('group/menu-sub-item relative', props.class)" > <slot /> </li> </template> -
app/components/ui/sidebar/SidebarProvider.vue 3.2 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { useEventListener, useMediaQuery } from '@vueuse/core' import { TooltipProvider } from 'reka-ui' import { computed, onMounted, ref } from 'vue' import { cn } from '@/lib/utils' import { provideSidebarContext, SIDEBAR_COOKIE_MAX_AGE, SIDEBAR_COOKIE_NAME, SIDEBAR_KEYBOARD_SHORTCUT, SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON, } from './utils' const props = defineProps<{ defaultOpen?: boolean open?: boolean class?: HTMLAttributes['class'] }>() const emits = defineEmits<{ 'update:open': [open: boolean] }>() // `useMediaQuery` returns `false` during SSR (no matchMedia) and // re-evaluates synchronously on the client's first render. If the // viewport is < 768px, SSR HTML has the desktop branch but the // client wants the mobile <Sheet> branch -- Vue throws a hydration // mismatch and the Sheet renders without its overlay (the desktop // sidebar's div is the parent the diff lands against). // // Gating on `mounted` makes both the server and the client's first // synchronous render produce the desktop branch unconditionally; // `onMounted` then flips the flag and `Sidebar.vue`'s v-else-if // re-runs against the real matchMedia signal. Cost: a brief // desktop-layout flash for mobile users on first paint. That's the // universal tradeoff for SSR-without-viewport-detection -- there is // no clean way to know the viewport on the server. const mediaMobile = useMediaQuery('(max-width: 768px)') const mounted = ref(false) onMounted(() => { mounted.value = true }) const isMobile = computed(() => mounted.value && mediaMobile.value) const openMobile = ref(false) // useVModel with passive:true returns a ref that appears writable but // doesn't actually update when no parent v-model is bound. Fall back to // a plain ref so toggleSidebar / setOpen work reliably. const open = ref<boolean>(true) 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 data-uipkge data-slot="sidebar-wrapper" :style="{ '--sidebar-width': SIDEBAR_WIDTH, '--sidebar-width-icon': SIDEBAR_WIDTH_ICON, }" :class="cn('group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full', props.class)" v-bind="$attrs" > <slot /> </div> </TooltipProvider> </template> -
app/components/ui/sidebar/SidebarRail.vue 1.4 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' import { useSidebar } from './utils' const props = defineProps<{ class?: HTMLAttributes['class'] }>() const { toggleSidebar } = useSidebar() </script> <template> <button type="button" data-sidebar="rail" data-uipkge data-slot="sidebar-rail" aria-label="Toggle Sidebar" :tabindex="-1" title="Toggle Sidebar" :class=" cn( 'hover:after:bg-sidebar-border focus-visible:ring-ring absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-colors duration-200 ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] focus-visible:ring-2 focus-visible:outline-none 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', 'hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full', '[[data-side=left][data-collapsible=offcanvas]_&]:-right-2', '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2', props.class, ) " @click="toggleSidebar" > <slot /> </button> </template> -
app/components/ui/sidebar/SidebarSeparator.vue 0.4 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' import { Separator } from '@/components/ui/separator' const props = defineProps<{ class?: HTMLAttributes['class'] }>() </script> <template> <Separator data-uipkge data-slot="sidebar-separator" data-sidebar="separator" :class="cn('bg-sidebar-border mx-2 w-auto', props.class)" > <slot /> </Separator> </template> -
app/components/ui/sidebar/SidebarTrigger.vue 0.7 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { PanelLeft } from 'lucide-vue-next' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' import { useSidebar } from './utils' const props = defineProps<{ class?: HTMLAttributes['class'] }>() const { toggleSidebar } = useSidebar() </script> <template> <Button data-sidebar="trigger" data-uipkge data-slot="sidebar-trigger" variant="ghost" size="icon" :class="cn('focus-visible:ring-ring h-7 w-7 focus-visible:ring-2 focus-visible:outline-none', props.class)" @click="toggleSidebar" > <PanelLeft /> <span class="sr-only">Toggle Sidebar</span> </Button> </template> -
app/components/ui/sidebar/sidebar.variants.ts 1.8 kB
import type { VariantProps } from 'class-variance-authority' import { cva } from 'class-variance-authority' /** * Variant definitions live in their own file (rather than the package * `index.ts`) so consuming Vue SFCs can import without creating a circular * dependency through the index. See card.variants.ts for the canonical * example + the SSR symptom that motivated the split. */ export const sidebarMenuButtonVariants = cva( 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm text-muted-foreground outline-hidden ring-sidebar-ring transition-[width,height,padding,color,background-color] hover:bg-sidebar-accent/50 hover:text-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-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-primary/10 data-[active=true]:font-medium data-[active=true]:text-primary data-[state=open]:hover:bg-sidebar-accent/50 data-[state=open]:hover:text-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-foreground', outline: 'bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]', }, size: { default: 'h-11 text-sm', sm: 'h-10 text-xs', lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!', }, }, defaultVariants: { variant: 'default', size: 'default', }, }, ) export type SidebarMenuButtonVariants = VariantProps<typeof sidebarMenuButtonVariants> -
app/components/ui/sidebar/index.ts 2 kB
import type { HTMLAttributes } from 'vue' export interface SidebarProps { side?: 'left' | 'right' variant?: 'sidebar' | 'floating' | 'inset' collapsible?: 'offcanvas' | 'icon' | 'none' class?: HTMLAttributes['class'] } export { default as Sidebar } from './Sidebar.vue' export { default as SidebarContent } from './SidebarContent.vue' export { default as SidebarFooter } from './SidebarFooter.vue' export { default as SidebarGroup } from './SidebarGroup.vue' export { default as SidebarGroupAction } from './SidebarGroupAction.vue' export { default as SidebarGroupContent } from './SidebarGroupContent.vue' export { default as SidebarGroupLabel } from './SidebarGroupLabel.vue' export { default as SidebarHeader } from './SidebarHeader.vue' export { default as SidebarInput } from './SidebarInput.vue' export { default as SidebarInset } from './SidebarInset.vue' export { default as SidebarMenu } from './SidebarMenu.vue' export { default as SidebarMenuAction } from './SidebarMenuAction.vue' export { default as SidebarMenuBadge } from './SidebarMenuBadge.vue' export { default as SidebarMenuButton } from './SidebarMenuButton.vue' export { default as SidebarMenuItem } from './SidebarMenuItem.vue' export { default as SidebarMenuSkeleton } from './SidebarMenuSkeleton.vue' export { default as SidebarMenuSub } from './SidebarMenuSub.vue' export { default as SidebarMenuSubButton } from './SidebarMenuSubButton.vue' export { default as SidebarMenuSubItem } from './SidebarMenuSubItem.vue' export { default as SidebarProvider } from './SidebarProvider.vue' export { default as SidebarRail } from './SidebarRail.vue' export { default as SidebarSeparator } from './SidebarSeparator.vue' export { default as SidebarTrigger } from './SidebarTrigger.vue' export { useSidebar } from './utils' // Re-export variant API from the sibling file (kept separate to avoid the // Component.vue <-> index.ts circular import that broke dev SSR for Card). export { sidebarMenuButtonVariants, type SidebarMenuButtonVariants } from './sidebar.variants' -
app/components/ui/sidebar/utils.ts 0.7 kB
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')
Raw manifest: https://uipkge.dev/r/vue/sidebar.json