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 sidebarSidebarContent
- Scrollable content.SidebarGroup
- Section within the SidebarContent.SidebarTrigger
- Trigger for the Sidebar
Usage
App.vue
[AppSidebar.vue] via components
[AppSidebar.vue] via slots
<template>
<NSidebarProvider>
<AppSidebar />
<main>
<NSidebarTrigger />
<NuxtPage />
</main>
</NSidebarProvider>
</template>
<template>
<NSidebar>
<NSidebarHeader>
<!-- header area -->
</NSidebarHeader>
<NSidebarContent>
<NSidebarGroup />
<NSidebarGroup />
</NSidebarContent>
<NSidebarFooter>
<!-- footer area -->
</NSidebarFooter>
</NSidebar>
</template>
<template>
<NSidebar>
<template #header>
<!-- header area -->
</template>
<template #content>
<NSidebarGroup />
<NSidebarGroup />
</template>
<template #footer>
<!-- footer area -->
</template>
</NSidebar>
</template>
Examples
Basic
Prop | Default | Type | Description |
---|---|---|---|
sheet | left | left ,right | The side of the sheet. Options are left and right . |
sidebar | sidebar | sidebar ,floating ,inset | The variant of the sidebar. |
collapsible | offcanvas | offcanvas ,icon ,none | Collapsible behavior. |
rail | true | boolean | Whether to display the sidebar rail for resizing. |
Preview
Dashboard.vue
AppSidebar.vue
TeamSwitcher.vue
NavMain.vue
NavProjects.vue
NavUser.vue
<script setup lang="ts">
import AppSidebar from '~/components/sidebar/basic/AppSidebar.vue'
const route = useRoute()
definePageMeta({
layout: false,
})
const breadcrumbItems = computed(() => {
const paths = route.path.split('/').filter(Boolean)
return paths.slice(1).map((path, index) => ({
label: path.charAt(0).toUpperCase() + path.slice(1),
to: `/${paths.slice(1, index + 2).join('/')}`, // we exclude the first path for docs only
}))
})
</script>
<template>
<NSidebarProvider>
<AppSidebar />
<NSidebarInset>
<header class="h-16 flex shrink-0 items-center justify-between gap-2 border-b transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar_wrapper:h-12">
<div class="flex items-center gap-2 px-4">
<NSidebarTrigger class="-ml-1" />
<NSeparator orientation="vertical" icon class="mx-0 mr-2 h-4" />
<NBreadcrumb
breadcrumb-active="text-black"
class="hidden lg:flex"
separator="i-lucide-slash"
:items="breadcrumbItems"
/>
</div>
</header>
</NSidebarInset>
</NSidebarProvider>
</template>
<script setup lang="ts">
import NavMain from './NavMain.vue'
import NavProjects from './NavProjects.vue'
import NavUser from './NavUser.vue'
import TeamSwitcher from './TeamSwitcher.vue'
// This is sample data.
const data = {
user: {
name: 'Phojie Rengel',
email: 'phojrengel@gmail.com',
avatar: '/images/avatar.png',
},
teams: [
{
name: 'Acme Inc',
logo: 'i-lucide-gallery-vertical-end',
plan: 'Enterprise',
},
{
name: 'Acme Corp.',
logo: 'i-lucide-audio-waveform',
plan: 'Startup',
},
{
name: 'Evil Corp.',
logo: 'i-lucide-command',
plan: 'Free',
},
],
navMain: [
{
title: 'Playground',
url: '#',
icon: 'i-lucide-square-terminal',
isActive: true,
items: [
{
title: 'History',
url: '#',
},
{
title: 'Starred',
url: '#',
},
{
title: 'Settings',
url: '#',
},
],
},
{
title: 'Models',
url: '#',
icon: 'i-lucide-bot',
items: [
{
title: 'Genesis',
url: '#',
},
{
title: 'Explorer',
url: '#',
},
{
title: 'Quantum',
url: '#',
},
],
},
{
title: 'Documentation',
url: '#',
icon: 'i-lucide-book-open',
items: [
{
title: 'Introduction',
url: '#',
},
{
title: 'Get Started',
url: '#',
},
{
title: 'Tutorials',
url: '#',
},
{
title: 'Changelog',
url: '#',
},
],
},
{
title: 'Settings',
url: '#',
icon: 'i-lucide-settings-2',
items: [
{
title: 'General',
url: '#',
},
{
title: 'Team',
url: '#',
},
{
title: 'Billing',
url: '#',
},
{
title: 'Limits',
url: '#',
},
],
},
],
projects: [
{
name: 'Design Engineering',
url: '#',
icon: 'i-lucide-frame',
},
{
name: 'Sales & Marketing',
url: '#',
icon: 'i-lucide-pie-chart',
},
{
name: 'Travel',
url: '#',
icon: 'i-lucide-map',
},
],
}
</script>
<template>
<NSidebar
collapsible="icon"
sidebar="sidebar"
sheet="left"
rail
>
<template #header>
<TeamSwitcher :teams="data.teams" />
</template>
<template #content>
<NavMain :items="data.navMain" />
<NavProjects :projects="data.projects" />
</template>
<template #footer>
<NavUser :user="data.user" />
</template>
</NSidebar>
</template>
<script setup lang="ts">
const props = defineProps<{
teams: {
name: string
logo: string
plan: string
}[]
}>()
const items = [
...props.teams.map(team => ({
label: team.name,
leading: team.logo,
})),
{},
{
label: 'Add New Team',
leading: 'i-lucide-plus',
},
]
const { isMobile } = useSidebar()
const activeTeam = ref(props.teams[0])
</script>
<template>
<NSidebarMenu>
<NSidebarMenuItem>
<NDropdownMenu
:_dropdown-menu-content="{
class: 'min-w-56 w-[--reka-dropdown-menu-trigger-width] rounded-lg',
align: 'start',
side: isMobile ? 'bottom' : 'right',
sideOffset: 4,
}"
:items
>
<NSidebarMenuButton
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div class="aspect-square flex items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground square-8">
<NIcon :name="activeTeam.logo" class="square-4" />
</div>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">
{{ activeTeam.name }}
</span>
<span class="truncate text-xs">{{ activeTeam.plan }}</span>
</div>
<NIcon name="i-lucide-chevrons-up-down" class="ml-auto" />
</NSidebarMenuButton>
</NDropdownMenu>
</NSidebarMenuItem>
</NSidebarMenu>
</template>
<script setup lang="ts">
defineProps<{
items: {
title: string
url: string
icon?: string
isActive?: boolean
items?: {
title: string
url: string
badge?: 'new' | 'hot' | 'beta' | 'deprecated'
}[]
}[]
}>()
</script>
<template>
<NSidebarGroup>
<NSidebarGroupLabel>Platform</NSidebarGroupLabel>
<NSidebarMenu>
<NCollapsible
v-for="item in items"
:key="item.title"
as-child
:default-open="item.isActive"
class="group/collapsible"
>
<NSidebarMenuItem>
<NCollapsibleTrigger as-child>
<NSidebarMenuButton :tooltip="item.title">
<NIcon v-if="item.icon" :name="item.icon" />
<span>{{ item.title }}</span>
<NIcon name="i-lucide-chevron-right" class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</NSidebarMenuButton>
</NCollapsibleTrigger>
<NCollapsibleContent>
<NSidebarMenuSub>
<NSidebarMenuSubItem v-for="subItem in item.items" :key="subItem.title">
<NSidebarMenuSubButton as-child>
<NLink :to="subItem.url">
<span>{{ subItem.title }}</span>
<NBadge
v-if="subItem.badge"
size="10px"
:badge="subItem.badge === 'new' ? 'solid-lime' : subItem.badge === 'hot' ? 'solid-orange' : subItem.badge === 'beta' ? 'solid-blue' : 'solid-red'"
class="ml-auto capitalize"
>
{{ subItem.badge }}
</NBadge>
</NLink>
</NSidebarMenuSubButton>
</NSidebarMenuSubItem>
</NSidebarMenuSub>
</NCollapsibleContent>
</NSidebarMenuItem>
</NCollapsible>
</NSidebarMenu>
</NSidebarGroup>
</template>
<script setup lang="ts">
defineProps<{
projects: {
name: string
url: string
icon: string
}[]
}>()
const { isMobile } = useSidebar()
</script>
<template>
<NSidebarGroup class="group-data-[collapsible=icon]:hidden">
<NSidebarGroupLabel>
Projects
</NSidebarGroupLabel>
<NSidebarMenu>
<NSidebarMenuItem v-for="item in projects" :key="item.name">
<NSidebarMenuButton as-child>
<a :href="item.url">
<NIcon :name="item.icon" />
<span>{{ item.name }}</span>
</a>
</NSidebarMenuButton>
<NDropdownMenu
:_dropdown-menu-content="{
side: isMobile ? 'bottom' : 'right',
align: isMobile ? 'end' : 'start',
sideOffset: 4,
}"
:items="[
{
leading: 'i-lucide-folder text-muted',
label: 'View Project',
},
{
leading: 'i-lucide-forward text-muted',
label: 'Share Project',
},
{},
{
leading: 'i-lucide-trash text-muted',
label: 'Delete Project',
},
]"
>
<NSidebarMenuAction show-on-hover>
<NIcon name="i-lucide-more-horizontal" class="text-sidebar-foreground/70" />
</NSidebarMenuAction>
</NDropdownMenu>
</NSidebarMenuItem>
<NSidebarMenuItem>
<NSidebarMenuButton class="text-sidebar-foreground/70">
<NIcon name="i-lucide-more-horizontal" class="text-sidebar-foreground/70" />
<span>More</span>
</NSidebarMenuButton>
</NSidebarMenuItem>
</NSidebarMenu>
</NSidebarGroup>
</template>
<script setup lang="ts">
defineProps<{
user: {
name: string
email: string
avatar: string
}
}>()
const navs = [
{
label: 'Upgrade to Pro',
leading: 'i-lucide-sparkles',
},
{},
{
label: 'Account',
leading: 'i-lucide-badge-check',
},
{
label: 'Billing',
leading: 'i-lucide-banknote',
},
{
label: 'Notifications',
leading: 'i-lucide-bell',
},
{
label: 'Log out',
leading: 'i-lucide-log-out',
},
]
const { isMobile } = useSidebar()
</script>
<template>
<NSidebarMenu>
<NSidebarMenuItem>
<NDropdownMenu
:items="navs"
:_dropdown-menu-content="{
class: 'min-w-56 w-[--reka-dropdown-menu-trigger-width] rounded-lg',
align: 'start',
side: isMobile ? 'bottom' : 'right',
sideOffset: 4,
}"
:_dropdown-menu-label="{
class: 'p-0 font-normal',
}"
>
<NSidebarMenuButton
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<NAvatar
square="8"
rounded="lg"
:src="user.avatar"
:alt="user.name"
/>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ user.name }}</span>
<span class="truncate text-xs">{{ user.email }}</span>
</div>
<NIcon name="i-lucide-chevron-down" class="ml-auto size-4" />
</NSidebarMenuButton>
<template #menu-label>
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<NAvatar
square="8"
rounded="lg"
:src="user.avatar"
:alt="user.name"
/>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ user.name }}</span>
<span class="truncate text-xs">{{ user.email }}</span>
</div>
</div>
</template>
</NDropdownMenu>
</NSidebarMenuItem>
</NSidebarMenu>
</template>
View the full basic example in a new tab.
A collapsible nested sidebar
Preview
Dashboard.vue
AppSidebar.vue
NavUser.vue
<script setup lang="ts">
import AppSidebar from '~/components/sidebar/collapsible-nested/AppSidebar.vue'
</script>
<template>
<NSidebarProvider
:style="{
'--sidebar-width': '350px',
}"
>
<AppSidebar />
<NSidebarInset>
<header class="sticky top-0 flex shrink-0 items-center gap-2 border-b bg-background p-4">
<NSidebarTrigger class="-ml-1" />
<NSeparator orientation="vertical" class="mx-0 h-4" />
<NBreadcrumb
:una="{
breadcrumbSeparator: 'hidden md:block',
}"
:items="[
{
label: 'All Inboxes',
to: '#',
class: 'hidden md:block',
},
{
label: 'Inbox',
to: '#',
},
]"
/>
</header>
<div class="flex flex-1 flex-col gap-4 p-4">
<div
v-for="index in 24"
:key="index"
class="aspect-video h-12 w-full rounded-lg bg-muted"
/>
</div>
</NSidebarInset>
</NSidebarProvider>
</template>
<script setup lang="ts">
import NavUser from './NavUser.vue'
// This is sample data
const data = {
user: {
name: 'Phojie Rengel',
email: 'phojrengel@gmail.com',
avatar: '/images/avatar.png',
},
navMain: [
{
title: 'Inbox',
url: '#',
icon: 'i-lucide-inbox',
isActive: true,
},
{
title: 'Drafts',
url: '#',
icon: 'i-lucide-file',
isActive: false,
},
{
title: 'Sent',
url: '#',
icon: 'i-lucide-send',
isActive: false,
},
{
title: 'Junk',
url: '#',
icon: 'i-lucide-archive-x',
isActive: false,
},
{
title: 'Trash',
url: '#',
icon: 'i-lucide-trash',
isActive: false,
},
],
mails: [
{
name: 'William Smith',
email: 'williamsmith@example.com',
subject: 'Meeting Tomorrow',
date: '09:34 AM',
teaser:
'Hi team, just a reminder about our meeting tomorrow at 10 AM.\nPlease come prepared with your project updates.',
},
{
name: 'Alice Smith',
email: 'alicesmith@example.com',
subject: 'Re: Project Update',
date: 'Yesterday',
teaser:
'Thanks for the update. The progress looks great so far.\nLet\'s schedule a call to discuss the next steps.',
},
{
name: 'Bob Johnson',
email: 'bobjohnson@example.com',
subject: 'Weekend Plans',
date: '2 days ago',
teaser:
'Hey everyone! I\'m thinking of organizing a team outing this weekend.\nWould you be interested in a hiking trip or a beach day?',
},
{
name: 'Emily Davis',
email: 'emilydavis@example.com',
subject: 'Re: Question about Budget',
date: '2 days ago',
teaser:
'I\'ve reviewed the budget numbers you sent over.\nCan we set up a quick call to discuss some potential adjustments?',
},
{
name: 'Michael Wilson',
email: 'michaelwilson@example.com',
subject: 'Important Announcement',
date: '1 week ago',
teaser:
'Please join us for an all-hands meeting this Friday at 3 PM.\nWe have some exciting news to share about the company\'s future.',
},
{
name: 'Sarah Brown',
email: 'sarahbrown@example.com',
subject: 'Re: Feedback on Proposal',
date: '1 week ago',
teaser:
'Thank you for sending over the proposal. I\'ve reviewed it and have some thoughts.\nCould we schedule a meeting to discuss my feedback in detail?',
},
{
name: 'David Lee',
email: 'davidlee@example.com',
subject: 'New Project Idea',
date: '1 week ago',
teaser:
'I\'ve been brainstorming and came up with an interesting project concept.\nDo you have time this week to discuss its potential impact and feasibility?',
},
{
name: 'Olivia Wilson',
email: 'oliviawilson@example.com',
subject: 'Vacation Plans',
date: '1 week ago',
teaser:
'Just a heads up that I\'ll be taking a two-week vacation next month.\nI\'ll make sure all my projects are up to date before I leave.',
},
{
name: 'James Martin',
email: 'jamesmartin@example.com',
subject: 'Re: Conference Registration',
date: '1 week ago',
teaser:
'I\'ve completed the registration for the upcoming tech conference.\nLet me know if you need any additional information from my end.',
},
{
name: 'Sophia White',
email: 'sophiawhite@example.com',
subject: 'Team Dinner',
date: '1 week ago',
teaser:
'To celebrate our recent project success, I\'d like to organize a team dinner.\nAre you available next Friday evening? Please let me know your preferences.',
},
],
}
const activeItem = ref(data.navMain[0])
const mails = ref(data.mails)
const { setOpen } = useSidebar()
</script>
<template>
<NSidebar
class="overflow-hidden [&>[data-sidebar=sidebar]]:flex-row"
collapsible="icon"
>
<!-- This is the first sidebar -->
<!-- We disable collapsible and adjust width to icon. -->
<!-- This will make the sidebar appear as icons. -->
<NSidebar
collapsible="none"
class="border-r !w-[calc(var(--sidebar-width-icon)_+_1px)]"
>
<NSidebarHeader>
<NSidebarMenu>
<NSidebarMenuItem>
<NSidebarMenuButton size="lg" as-child class="md:h-8 md:p-0">
<NLink to="#">
<div class="aspect-square flex items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground square-8">
<NIcon name="i-lucide-command" class="square-4" />
</div>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">Acme Inc</span>
<span class="truncate text-xs">Enterprise</span>
</div>
</NLink>
</NSidebarMenuButton>
</NSidebarMenuItem>
</NSidebarMenu>
</NSidebarHeader>
<NSidebarContent>
<NSidebarGroup>
<NSidebarGroupContent class="px-1.5 md:px-0">
<NSidebarMenu>
<NSidebarMenuItem v-for="item in data.navMain" :key="item.title">
<NSidebarMenuButton
:tooltip="h('div', { hidden: false }, item.title)"
:is-active="activeItem.title === item.title"
class="px-2.5 md:px-2"
@click="() => {
activeItem = item
const mail = data.mails.sort(() => Math.random() - 0.5)
mails = mail.slice(0, Math.max(5, Math.floor(Math.random() * 10) + 1))
setOpen(true)
}"
>
<NIcon :name="item.icon" />
<span>{{ item.title }}</span>
</NSidebarMenuButton>
</NSidebarMenuItem>
</NSidebarMenu>
</NSidebarGroupContent>
</NSidebarGroup>
</NSidebarContent>
<NSidebarFooter>
<NavUser :user="data.user" />
</NSidebarFooter>
</NSidebar>
<!-- This is the second sidebar -->
<!-- We disable collapsible and let it fill remaining space -->
<NSidebar collapsible="none" class="hidden flex-1 md:flex">
<NSidebarHeader class="gap-3.5 border-b p-4">
<div class="w-full flex items-center justify-between">
<div class="text-base text-foreground font-medium">
{{ activeItem.title }}
</div>
<NLabel class="flex items-center gap-2 text-sm">
<span>Unreads</span>
<NSwitch class="shadow-none" />
</NLabel>
</div>
<NSidebarInput placeholder="Type to search..." />
</NSidebarHeader>
<NSidebarContent>
<NSidebarGroup class="px-0">
<NSidebarGroupContent>
<NLink
v-for="mail in mails"
:key="mail.email"
href="#"
class="flex flex-col items-start gap-2 whitespace-nowrap border-b p-4 text-sm leading-tight last:border-b-0 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
>
<div class="w-full flex items-center gap-2">
<span>{{ mail.name }}</span>
<span class="ml-auto text-xs">{{ mail.date }}</span>
</div>
<span class="font-medium">{{ mail.subject }}</span>
<span class="line-clamp-2 w-[260px] whitespace-break-spaces text-xs">
{{ mail.teaser }}
</span>
</NLink>
</NSidebarGroupContent>
</NSidebarGroup>
</NSidebarContent>
</NSidebar>
</NSidebar>
</template>
<script setup lang="ts">
defineProps<{
user: {
name: string
email: string
avatar: string
}
}>()
const navs = [
{
label: 'Upgrade to Pro',
leading: 'i-lucide-sparkles',
},
{},
{
label: 'Account',
leading: 'i-lucide-badge-check',
},
{
label: 'Billing',
leading: 'i-lucide-banknote',
},
{
label: 'Notifications',
leading: 'i-lucide-bell',
},
{
label: 'Log out',
leading: 'i-lucide-log-out',
},
]
const { isMobile } = useSidebar()
</script>
<template>
<NSidebarMenu>
<NSidebarMenuItem>
<NDropdownMenu
:items="navs"
:_dropdown-menu-content="{
class: 'min-w-56 w-[--reka-dropdown-menu-trigger-width] rounded-lg',
align: 'start',
side: isMobile ? 'bottom' : 'right',
sideOffset: 4,
}"
:_dropdown-menu-label="{
class: 'p-0 font-normal',
}"
>
<NSidebarMenuButton
size="lg"
class="md:h-8 data-[state=open]:bg-sidebar-accent md:p-0 data-[state=open]:text-sidebar-accent-foreground"
>
<NAvatar
square="8"
rounded="lg"
:src="user.avatar"
:alt="user.name"
/>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ user.name }}</span>
<span class="truncate text-xs">{{ user.email }}</span>
</div>
<NIcon name="i-lucide-chevron-down" class="ml-auto size-4" />
</NSidebarMenuButton>
<template #menu-label>
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<NAvatar
square="8"
rounded="lg"
:src="user.avatar"
:alt="user.name"
/>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ user.name }}</span>
<span class="truncate text-xs">{{ user.email }}</span>
</div>
</div>
</template>
</NDropdownMenu>
</NSidebarMenuItem>
</NSidebarMenu>
</template>
View the full collapsible nested sidebar example in a new tab.
Customization
You can customize the sidebar using the following sub components props and una
prop.
Name | Type | Description |
---|---|---|
_sidebarContent | NSidebarContentProps | Props for the content component. |
_sidebarHeader | NSidebarHeaderProps | Props for the header component. |
_sidebarFooter | NSidebarFooterProps | Props for the footer component. |
_sidebarRail | NSidebarRailProps | Props for the rail component. |
una | NSidebarUnaProps | UnaUI preset configuration for all sidebar components. |
Read more in Sheet props
Slots
Name | Props | Description |
---|---|---|
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>
<script setup lang="ts">
import type { NSidebarProps } from '../../types'
import { createReusableTemplate } from '@vueuse/core'
import { SIDEBAR_WIDTH_MOBILE, useSidebar } from '../../composables/useSidebar'
import { cn } from '../../utils'
import Sheet from '../sheet/Sheet.vue'
import SidebarContent from './SidebarContent.vue'
import SidebarFooter from './SidebarFooter.vue'
import SidebarHeader from './SidebarHeader.vue'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<NSidebarProps>(), {
sheet: 'left',
sidebar: 'sidebar',
collapsible: 'offcanvas',
rail: true,
})
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
const [DefineSlot, ReuseSlot] = createReusableTemplate()
</script>
<template>
<DefineSlot>
<slot>
<SidebarHeader v-bind="props._sidebarHeader">
<slot name="header" />
</SidebarHeader>
<SidebarContent v-bind="props._sidebarContent">
<slot name="content" />
</SidebarContent>
<SidebarFooter v-bind="props._sidebarFooter">
<slot name="footer" />
</SidebarFooter>
<NSidebarRail v-if="rail" v-bind="props._sidebarRail" />
</slot>
</DefineSlot>
<div
v-if="collapsible === 'none'"
:class="cn(
'sidebar-collapsible-none',
props.una?.sidebar,
props.class,
)"
v-bind="$attrs"
>
<ReuseSlot />
</div>
<Sheet
v-else-if="isMobile"
:open="openMobile"
v-bind="$attrs"
:_sheet-content="{
dataSidebar: 'sidebar',
dataMobile: true,
sheet,
class: 'sidebar-mobile',
style: {
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
},
...props._sheetContent,
}"
@update:open="setOpenMobile"
>
<div class="sidebar-mobile-inner">
<ReuseSlot />
</div>
</Sheet>
<div
v-else :class="cn('group peer sidebar-desktop')"
:data-state="state"
:data-collapsible="state === 'collapsed' ? collapsible : ''"
:data-variant="sidebar"
:data-side="sheet"
>
<!-- This is what handles the sidebar gap on desktop -->
<div
:class="cn(
'sidebar-desktop-gap',
sidebar === 'floating' || sidebar === 'inset'
? 'sidebar-desktop-gap-floating'
: 'sidebar-desktop-gap-default',
)"
/>
<div
:class="cn(
'sidebar-desktop-position',
sheet === 'left'
? 'sidebar-desktop-position-left'
: 'sidebar-desktop-position-right',
// Adjust the padding for floating and inset variants.
sidebar === 'floating' || sidebar === 'inset'
? 'sidebar-desktop-padding-floating'
: 'sidebar-desktop-padding-default',
props.una?.sidebar,
props.class,
)"
v-bind="$attrs"
>
<div
data-sidebar="sidebar"
class="sidebar-desktop-inner"
>
<ReuseSlot />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { NSidebarContentProps } from '../../types'
import { cn } from '../../utils'
import ScrollArea from '../scroll-area/ScrollArea.vue'
const props = defineProps<NSidebarContentProps>()
</script>
<template>
<div
data-sidebar="content"
:class="cn(
'sidebar-content',
props.una?.sidebarContent,
props.class,
)"
>
<ScrollArea>
<slot />
</ScrollArea>
</div>
</template>
<script setup lang="ts">
import type { NSidebarHeaderProps } from '../../types'
import { cn } from '../../utils'
const props = defineProps<NSidebarHeaderProps>()
</script>
<template>
<div
data-sidebar="header"
:class="cn(
'sidebar-header',
props.una?.sidebarHeader,
props.class,
)"
>
<slot />
</div>
</template>
<script setup lang="ts">
import type { NSidebarFooterProps } from '../../types'
import { cn } from '../../utils'
const props = defineProps<NSidebarFooterProps>()
</script>
<template>
<div
data-sidebar="footer"
:class="cn(
'sidebar-footer',
props.una?.sidebarFooter,
props.class,
)"
>
<slot />
</div>
</template>
<script setup lang="ts" generic="T extends { id?: string | number } | Record<string, any>">
import type { NSidebarMenuProps } from '../../types'
import { cn } from '../../utils'
import SidebarMenuItem from './SidebarMenuItem.vue'
const props = defineProps<NSidebarMenuProps<T>>()
function getKey(item: T) {
if (props.itemKey)
return item[props.itemKey]
return 'id' in item ? item.id : JSON.stringify(item)
}
</script>
<template>
<ul
data-sidebar="menu"
:class="cn(
'sidebar-menu',
props.una?.sidebarMenu,
props.class,
)"
>
<slot>
<SidebarMenuItem v-for="item in props.items" :key="getKey(item)">
<slot name="item" :item="item" />
</SidebarMenuItem>
</slot>
</ul>
</template>
<script setup lang="ts">
import type { NSidebarMenuItemProps } from '../../types'
import { cn } from '../../utils'
const props = defineProps<NSidebarMenuItemProps>()
</script>
<template>
<li
data-sidebar="menu-item"
:class="cn(
'group/menu_item sidebar-menu-item',
props.una?.sidebarMenuItem,
props.class,
)"
>
<slot />
</li>
</template>
<script setup lang="ts">
import type { NSidebarMenuButtonProps } from '../../types'
import { computed } from 'vue'
import { useSidebar } from '../../composables/useSidebar'
import TooltipContent from '../elements/tooltip/TooltipContent.vue'
import TooltipRoot from '../elements/tooltip/TooltipRoot.vue'
import TooltipTrigger from '../elements/tooltip/TooltipTrigger.vue'
import SidebarMenuButtonChild from './SidebarMenuButtonChild.vue'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<NSidebarMenuButtonProps>(), {
as: 'button',
variant: 'default',
size: 'default',
})
const { isMobile, state } = useSidebar()
const delegatedProps = computed(() => {
const { tooltip, una, ...delegated } = props
return delegated
})
const unaProps = computed(() => {
return {
una: {
sidebarMenuButtonChild: props.una?.sidebarMenuButtonChild,
},
}
})
</script>
<template>
<SidebarMenuButtonChild v-if="!tooltip" v-bind="{ ...delegatedProps, ...unaProps, ...$attrs }">
<slot />
</SidebarMenuButtonChild>
<TooltipRoot v-else>
<TooltipTrigger as-child>
<SidebarMenuButtonChild v-bind="{ ...delegatedProps, ...unaProps, ...$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>
</TooltipRoot>
</template>
<script setup lang="ts">
import type { VariantProps } from 'class-variance-authority'
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { NSidebarMenuButtonChildProps } from '../../types'
import { cva } from 'class-variance-authority'
import { Primitive } from 'reka-ui'
import { cn } from '../../utils'
const props = withDefaults(defineProps<NSidebarMenuButtonChildProps>(), {
as: 'button',
variant: 'default',
size: 'default',
})
const sidebarMenuButtonVariants = cva(
'peer-menu_button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu_item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!square-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>span[icon-base]]:square-4',
{
variants: {
variant: {
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
outline:
'bg-background shadow-[0_0_0_1px_rgb(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_rgb(var(--sidebar-accent))]',
},
size: {
default: 'h-8 text-sm',
sm: 'h-7 text-xs',
lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
type SidebarMenuButtonVariants = VariantProps<typeof sidebarMenuButtonVariants>
export interface SidebarMenuButtonProps extends PrimitiveProps {
variant?: SidebarMenuButtonVariants['variant']
size?: SidebarMenuButtonVariants['size']
isActive?: boolean
class?: HTMLAttributes['class']
}
</script>
<template>
<Primitive
data-sidebar="menu_button"
:data-size="size"
:data-active="isActive"
:class="cn(
sidebarMenuButtonVariants({ variant, size }),
props.una?.sidebarMenuButtonChild,
props.class,
)"
:as="as"
:as-child="asChild"
v-bind="$attrs"
>
<slot />
</Primitive>
</template>
<script setup lang="ts">
import type { NSidebarMenuSubButtonProps } from '../../types'
import { Primitive } from 'reka-ui'
import { cn } from '../../utils'
const props = withDefaults(defineProps<NSidebarMenuSubButtonProps>(), {
as: 'a',
size: 'md',
})
</script>
<template>
<Primitive
data-sidebar="menu-sub-button"
:as="as"
:as-child="asChild"
:data-size="size"
:data-active="isActive"
:class="cn(
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>span[icon-base]]:square-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground',
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
size === 'sm' && 'text-xs',
size === 'md' && 'text-sm',
'group-data-[collapsible=icon]:hidden',
props.una?.sidebarMenuSubButton,
props.class,
)"
>
<slot />
</Primitive>
</template>
<script setup lang="ts">
import type { NSidebarMenuSubProps } from '../../types'
import { cn } from '../../utils'
const props = defineProps<NSidebarMenuSubProps>()
</script>
<template>
<ul
data-sidebar="menu-badge"
:class="cn(
'sidebar-menu-sub',
props.una?.sidebarMenuSub,
props.class,
)"
>
<slot />
</ul>
</template>
<script setup lang="ts">
import type { NSidebarMenuSubItemProps } from '../../types'
import { cn } from '../../utils'
const props = defineProps<NSidebarMenuSubItemProps>()
</script>
<template>
<li
:class="cn(
'sidebar-menu-sub-item',
props.una?.sidebarMenuSubItem,
props.class,
)"
>
<slot />
</li>
</template>
<script setup lang="ts">
import type { NSidebarMenuActionProps } from '../../types'
import { Primitive } from 'reka-ui'
import { cn } from '../../utils'
const props = withDefaults(defineProps<NSidebarMenuActionProps>(), {
as: 'button',
})
</script>
<template>
<Primitive
data-sidebar="menu-action"
:class="cn(
'sidebar-menu-action',
showOnHover && 'sidebar-menu-action-show-on-hover',
props.una?.sidebarMenuAction,
props.class,
)"
:as="as"
:as-child="asChild"
>
<slot />
</Primitive>
</template>
<script setup lang="ts">
import type { NSidebarMenuBadgeProps } from '../../types'
import { cn } from '../../utils'
const props = defineProps<NSidebarMenuBadgeProps>()
</script>
<template>
<div
data-sidebar="menu-badge"
:class="cn(
'sidebar-menu-badge',
props.una?.sidebarMenuBadge,
props.class,
)"
>
<slot />
</div>
</template>
<script setup lang="ts">
import type { NSidebarGroupProps } from '../../types'
import { cn } from '../../utils'
import SidebarGroupLabel from './SidebarGroupLabel.vue'
const props = defineProps<NSidebarGroupProps>()
</script>
<template>
<div
data-sidebar="group"
:class="cn(
'sidebar-group',
props.una?.sidebarGroup,
props.class,
)"
>
<slot name="root">
<SidebarGroupLabel v-if="props.label">
<slot name="label">
{{ props.label }}
</slot>
</SidebarGroupLabel>
<slot />
</slot>
</div>
</template>
<script setup lang="ts">
import type { NSidebarGroupLabelProps } from '../../types'
import { Primitive } from 'reka-ui'
import { cn } from '../../utils'
const props = withDefaults(defineProps<NSidebarGroupLabelProps>(), {
as: 'div',
asChild: false,
})
</script>
<template>
<Primitive
data-sidebar="group-label"
:as="as"
:as-child="asChild"
:class="cn(
'sidebar-group-label',
props.una?.sidebarGroupLabel,
props.class,
)"
>
<slot />
</Primitive>
</template>
<script setup lang="ts">
import type { NSidebarGroupContentProps } from '../../types'
import { cn } from '../../utils'
const props = defineProps<NSidebarGroupContentProps>()
</script>
<template>
<div
data-sidebar="group-content"
:class="cn(
'sidebar-group-content',
props.una?.sidebarGroupContent,
props.class,
)"
>
<slot />
</div>
</template>
<script setup lang="ts">
import type { NSidebarRailProps } from '../../types'
import { useSidebar } from '../../composables/useSidebar'
import { cn } from '../../utils'
const props = defineProps<NSidebarRailProps>()
const { toggleSidebar } = useSidebar()
</script>
<template>
<button
data-sidebar="rail"
aria-label="Toggle Sidebar"
:tabindex="-1"
title="Toggle Sidebar"
:class="cn(
'sidebar-rail',
props.una?.sidebarRail,
props.class,
)"
@click="toggleSidebar"
>
<slot />
</button>
</template>