Sidebar
Usage
The Sidebar component is a standalone, fixed sidebar that pushes the page content. On desktop, it renders inline and can be collapsed; on mobile, it opens a Modal, Slideover or Drawer component.
Use the header, default and footer slots to customize the sidebar content. The v-model:open directive is viewport-aware: on desktop it controls the expanded/collapsed state, on mobile it controls the menu.
<script setup lang="ts">
import type { DropdownMenuItem, NavigationMenuItem } from 'pohon-ui'
const open = ref(true)
const colorMode = useColorMode()
const teams = ref([
{
label: 'Nuxt',
avatar: {
src: 'https://github.com/nuxt.png',
alt: 'Nuxt'
}
},
{
label: 'Vue',
avatar: {
src: 'https://github.com/vuejs.png',
alt: 'Vue'
}
},
{
label: 'UnJS',
avatar: {
src: 'https://github.com/unjs.png',
alt: 'UnJS'
}
}
])
const selectedTeam = ref(teams.value[0])
const teamsItems = computed<Array<Array<DropdownMenuItem>>>(() => {
return [
teams.value.map((team, index) => ({
...team,
kbds: ['meta', String(index + 1)],
onSelect() {
selectedTeam.value = team
}
})),
[
{
label: 'Create team',
icon: 'i-lucide-circle-plus'
}
]
]
})
function getItems(state: 'collapsed' | 'expanded') {
return [
{
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4'
},
{
label: 'Issues',
icon: 'i-lucide-square-dot'
},
{
label: 'Activity',
icon: 'i-lucide-square-activity'
},
{
label: 'Settings',
icon: 'i-lucide-settings',
defaultOpen: true,
children:
state === 'expanded'
? [
{
label: 'General',
icon: 'i-lucide-house'
},
{
label: 'Team',
icon: 'i-lucide-users'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
}
]
: []
}
] satisfies Array<NavigationMenuItem>
}
const user = ref({
name: 'Benjamin Canac',
avatar: {
src: 'https://github.com/benjamincanac.png',
alt: 'Benjamin Canac'
}
})
const userItems = computed<Array<Array<DropdownMenuItem>>>(() => [
[
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-settings',
to: '/settings'
}
],
[
{
label: 'Appearance',
icon: 'i-lucide-sun-moon',
children: [
{
label: 'Light',
icon: 'i-lucide-sun',
type: 'checkbox',
checked: colorMode.value === 'light',
onUpdateChecked(checked: boolean) {
if (checked) {
colorMode.preference = 'light'
}
},
onSelect(e: Event) {
e.preventDefault()
}
},
{
label: 'Dark',
icon: 'i-lucide-moon',
type: 'checkbox',
checked: colorMode.value === 'dark',
onUpdateChecked(checked: boolean) {
if (checked) {
colorMode.preference = 'dark'
}
},
onSelect(e: Event) {
e.preventDefault()
}
}
]
}
],
[
{
label: 'GitHub',
icon: 'i-simple-icons-github',
to: 'https://github.com/vinicunca/pohon',
target: '_blank'
},
{
label: 'Log out',
icon: 'i-lucide-log-out'
}
]
])
defineShortcuts(extractShortcuts(teamsItems.value))
</script>
<template>
<div class="flex flex-1">
<PSidebar
v-model:open="open"
collapsible="icon"
rail
:ui="{
container: 'h-full',
inner: 'bg-background-elevated/25 divide-transparent',
body: 'py-0'
}"
>
<template #header>
<PDropdownMenu
:items="teamsItems"
:content="{ align: 'start', collisionPadding: 12 }"
:ui="{ content: 'w-(--akar-dropdown-menu-trigger-width) min-w-48' }"
>
<PButton
v-bind="selectedTeam"
trailing-icon="i-lucide-chevrons-up-down"
color="neutral"
variant="ghost"
square
class="w-full overflow-hidden data-[state=open]:bg-background-elevated"
:ui="{
trailingIcon: 'color-text-dimmed ms-auto'
}"
/>
</PDropdownMenu>
</template>
<template #default="{ state }">
<PNavigationMenu
:key="state"
:items="getItems(state)"
orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }"
/>
</template>
<template #footer>
<PDropdownMenu
:items="userItems"
:content="{ align: 'center', collisionPadding: 12 }"
:ui="{ content: 'w-(--akar-dropdown-menu-trigger-width) min-w-48' }"
>
<PButton
v-bind="user"
:label="user?.name"
trailing-icon="i-lucide-chevrons-up-down"
color="neutral"
variant="ghost"
square
class="w-full overflow-hidden data-[state=open]:bg-background-elevated"
:ui="{
trailingIcon: 'color-text-dimmed ms-auto'
}"
/>
</PDropdownMenu>
</template>
</PSidebar>
<div class="flex flex-1 flex-col">
<div class="px-4 border-b border-border flex shrink-0 h-(--ui-header-height) items-center">
<PButton
icon="i-lucide-panel-left"
color="neutral"
variant="ghost"
aria-label="Toggle sidebar"
@click="open = !open"
/>
</div>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
</div>
</template><script setup lang="ts">
import { ref, computed } from 'vue'
import { useColorMode } from '@vueuse/core'
import type { DropdownMenuItem, NavigationMenuItem } from 'pohon-ui'
const open = ref(true)
const colorMode = useColorMode()
const teams = ref([
{
label: 'Nuxt',
avatar: {
src: 'https://github.com/nuxt.png',
alt: 'Nuxt'
}
},
{
label: 'Vue',
avatar: {
src: 'https://github.com/vuejs.png',
alt: 'Vue'
}
},
{
label: 'UnJS',
avatar: {
src: 'https://github.com/unjs.png',
alt: 'UnJS'
}
}
])
const selectedTeam = ref(teams.value[0])
const teamsItems = computed<Array<Array<DropdownMenuItem>>>(() => {
return [
teams.value.map((team, index) => ({
...team,
kbds: ['meta', String(index + 1)],
onSelect() {
selectedTeam.value = team
}
})),
[
{
label: 'Create team',
icon: 'i-lucide-circle-plus'
}
]
]
})
function getItems(state: 'collapsed' | 'expanded') {
return [
{
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4'
},
{
label: 'Issues',
icon: 'i-lucide-square-dot'
},
{
label: 'Activity',
icon: 'i-lucide-square-activity'
},
{
label: 'Settings',
icon: 'i-lucide-settings',
defaultOpen: true,
children:
state === 'expanded'
? [
{
label: 'General',
icon: 'i-lucide-house'
},
{
label: 'Team',
icon: 'i-lucide-users'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
}
]
: []
}
] satisfies Array<NavigationMenuItem>
}
const user = ref({
name: 'Benjamin Canac',
avatar: {
src: 'https://github.com/benjamincanac.png',
alt: 'Benjamin Canac'
}
})
const userItems = computed<Array<Array<DropdownMenuItem>>>(() => [
[
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-settings',
to: '/settings'
}
],
[
{
label: 'Appearance',
icon: 'i-lucide-sun-moon',
children: [
{
label: 'Light',
icon: 'i-lucide-sun',
type: 'checkbox',
checked: colorMode.value === 'light',
onUpdateChecked(checked: boolean) {
if (checked) {
colorMode.preference = 'light'
}
},
onSelect(e: Event) {
e.preventDefault()
}
},
{
label: 'Dark',
icon: 'i-lucide-moon',
type: 'checkbox',
checked: colorMode.value === 'dark',
onUpdateChecked(checked: boolean) {
if (checked) {
colorMode.preference = 'dark'
}
},
onSelect(e: Event) {
e.preventDefault()
}
}
]
}
],
[
{
label: 'GitHub',
icon: 'i-simple-icons-github',
to: 'https://github.com/vinicunca/pohon',
target: '_blank'
},
{
label: 'Log out',
icon: 'i-lucide-log-out'
}
]
])
defineShortcuts(extractShortcuts(teamsItems.value))
</script>
<template>
<div class="flex flex-1">
<PSidebar
v-model:open="open"
collapsible="icon"
rail
:ui="{
container: 'h-full',
inner: 'bg-background-elevated/25 divide-transparent',
body: 'py-0'
}"
>
<template #header>
<PDropdownMenu
:items="teamsItems"
:content="{ align: 'start', collisionPadding: 12 }"
:ui="{ content: 'w-(--akar-dropdown-menu-trigger-width) min-w-48' }"
>
<PButton
v-bind="selectedTeam"
trailing-icon="i-lucide-chevrons-up-down"
color="neutral"
variant="ghost"
square
class="w-full overflow-hidden data-[state=open]:bg-background-elevated"
:ui="{
trailingIcon: 'color-text-dimmed ms-auto'
}"
/>
</PDropdownMenu>
</template>
<template #default="{ state }">
<PNavigationMenu
:key="state"
:items="getItems(state)"
orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }"
/>
</template>
<template #footer>
<PDropdownMenu
:items="userItems"
:content="{ align: 'center', collisionPadding: 12 }"
:ui="{ content: 'w-(--akar-dropdown-menu-trigger-width) min-w-48' }"
>
<PButton
v-bind="user"
:label="user?.name"
trailing-icon="i-lucide-chevrons-up-down"
color="neutral"
variant="ghost"
square
class="w-full overflow-hidden data-[state=open]:bg-background-elevated"
:ui="{
trailingIcon: 'color-text-dimmed ms-auto'
}"
/>
</PDropdownMenu>
</template>
</PSidebar>
<div class="flex flex-1 flex-col">
<div class="px-4 border-b border-border flex shrink-0 h-(--ui-header-height) items-center">
<PButton
icon="i-lucide-panel-left"
color="neutral"
variant="ghost"
aria-label="Toggle sidebar"
@click="open = !open"
/>
</div>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
</div>
</template>Variant
Use the variant prop to change the visual style of the sidebar. Defaults to sidebar.
<script setup lang="ts">
import type { NavigationMenuItem, SidebarProps } from 'pohon-ui'
// Ignore the props for the example
defineProps<Pick<SidebarProps, 'variant' | 'collapsible' | 'side'>>()
const open = ref(true)
const items: Array<NavigationMenuItem> = [
{
label: 'Home',
icon: 'i-lucide-house',
active: true
},
{
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4'
},
{
label: 'Contacts',
icon: 'i-lucide-users'
}
]
</script>
<template>
<div
class="flex flex-1"
:class="[
variant === 'inset' && 'bg-neutral-50 dark:bg-neutral-950',
side === 'right' && 'flex-row-reverse'
]"
>
<PSidebar
v-model:open="open"
:variant="variant"
:collapsible="collapsible"
:side="side"
:ui="{
container: 'h-full'
}"
>
<template #header>
<PIcon name="i-logos-nuxt-icon" class="size-8" />
</template>
<PNavigationMenu
:items="items"
orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }"
/>
</PSidebar>
<div
class="lg:peer-data-[variant=inset]:not-peer-data-[collapsible=offcanvas]:ms-0 bg-background flex flex-1 flex-col overflow-hidden peer-data-[variant=inset]:m-4 peer-data-[variant=inset]:rounded-xl peer-data-[variant=inset]:ring peer-data-[variant=inset]:ring-border peer-data-[variant=inset]:shadow-sm lg:peer-data-[variant=floating]:my-4"
>
<div
class="px-4 flex shrink-0 h-(--ui-header-height) items-center"
:class="[
variant !== 'floating' && 'border-b border-border',
side === 'right' && 'justify-end'
]"
>
<PButton
:icon="side === 'left' ? 'i-lucide-panel-left' : 'i-lucide-panel-right'"
color="neutral"
variant="ghost"
aria-label="Toggle sidebar"
@click="open = !open"
/>
</div>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
</div>
</template><script setup lang="ts">
import { ref } from 'vue'
import type { NavigationMenuItem, SidebarProps } from 'pohon-ui'
// Ignore the props for the example
defineProps<Pick<SidebarProps, 'variant' | 'collapsible' | 'side'>>()
const open = ref(true)
const items: Array<NavigationMenuItem> = [
{
label: 'Home',
icon: 'i-lucide-house',
active: true
},
{
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4'
},
{
label: 'Contacts',
icon: 'i-lucide-users'
}
]
</script>
<template>
<div
class="flex flex-1"
:class="[
variant === 'inset' && 'bg-neutral-50 dark:bg-neutral-950',
side === 'right' && 'flex-row-reverse'
]"
>
<PSidebar
v-model:open="open"
:variant="variant"
:collapsible="collapsible"
:side="side"
:ui="{
container: 'h-full'
}"
>
<template #header>
<PIcon name="i-logos-nuxt-icon" class="size-8" />
</template>
<PNavigationMenu
:items="items"
orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }"
/>
</PSidebar>
<div
class="lg:peer-data-[variant=inset]:not-peer-data-[collapsible=offcanvas]:ms-0 bg-background flex flex-1 flex-col overflow-hidden peer-data-[variant=inset]:m-4 peer-data-[variant=inset]:rounded-xl peer-data-[variant=inset]:ring peer-data-[variant=inset]:ring-border peer-data-[variant=inset]:shadow-sm lg:peer-data-[variant=floating]:my-4"
>
<div
class="px-4 flex shrink-0 h-(--ui-header-height) items-center"
:class="[
variant !== 'floating' && 'border-b border-border',
side === 'right' && 'justify-end'
]"
>
<PButton
:icon="side === 'left' ? 'i-lucide-panel-left' : 'i-lucide-panel-right'"
color="neutral"
variant="ghost"
aria-label="Toggle sidebar"
@click="open = !open"
/>
</div>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
</div>
</template>Collapsible
Use the collapsible prop to change the collapse behavior of the sidebar. Defaults to offcanvas.
offcanvas: The sidebar slides out of view completely.icon: The sidebar shrinks to icon-only width.none: The sidebar is not collapsible.
<script setup lang="ts">
import type { NavigationMenuItem, SidebarProps } from 'pohon-ui'
// Ignore the props for the example
defineProps<Pick<SidebarProps, 'variant' | 'collapsible' | 'side'>>()
const open = ref(true)
const items: Array<NavigationMenuItem> = [
{
label: 'Home',
icon: 'i-lucide-house',
active: true
},
{
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4'
},
{
label: 'Contacts',
icon: 'i-lucide-users'
}
]
</script>
<template>
<div
class="flex flex-1"
:class="[
variant === 'inset' && 'bg-neutral-50 dark:bg-neutral-950',
side === 'right' && 'flex-row-reverse'
]"
>
<PSidebar
v-model:open="open"
:variant="variant"
:collapsible="collapsible"
:side="side"
:ui="{
container: 'h-full'
}"
>
<template #header>
<PIcon name="i-logos-nuxt-icon" class="size-8" />
</template>
<PNavigationMenu
:items="items"
orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }"
/>
</PSidebar>
<div
class="lg:peer-data-[variant=inset]:not-peer-data-[collapsible=offcanvas]:ms-0 bg-background flex flex-1 flex-col overflow-hidden peer-data-[variant=inset]:m-4 peer-data-[variant=inset]:rounded-xl peer-data-[variant=inset]:ring peer-data-[variant=inset]:ring-border peer-data-[variant=inset]:shadow-sm lg:peer-data-[variant=floating]:my-4"
>
<div
class="px-4 flex shrink-0 h-(--ui-header-height) items-center"
:class="[
variant !== 'floating' && 'border-b border-border',
side === 'right' && 'justify-end'
]"
>
<PButton
:icon="side === 'left' ? 'i-lucide-panel-left' : 'i-lucide-panel-right'"
color="neutral"
variant="ghost"
aria-label="Toggle sidebar"
@click="open = !open"
/>
</div>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
</div>
</template><script setup lang="ts">
import { ref } from 'vue'
import type { NavigationMenuItem, SidebarProps } from 'pohon-ui'
// Ignore the props for the example
defineProps<Pick<SidebarProps, 'variant' | 'collapsible' | 'side'>>()
const open = ref(true)
const items: Array<NavigationMenuItem> = [
{
label: 'Home',
icon: 'i-lucide-house',
active: true
},
{
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4'
},
{
label: 'Contacts',
icon: 'i-lucide-users'
}
]
</script>
<template>
<div
class="flex flex-1"
:class="[
variant === 'inset' && 'bg-neutral-50 dark:bg-neutral-950',
side === 'right' && 'flex-row-reverse'
]"
>
<PSidebar
v-model:open="open"
:variant="variant"
:collapsible="collapsible"
:side="side"
:ui="{
container: 'h-full'
}"
>
<template #header>
<PIcon name="i-logos-nuxt-icon" class="size-8" />
</template>
<PNavigationMenu
:items="items"
orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }"
/>
</PSidebar>
<div
class="lg:peer-data-[variant=inset]:not-peer-data-[collapsible=offcanvas]:ms-0 bg-background flex flex-1 flex-col overflow-hidden peer-data-[variant=inset]:m-4 peer-data-[variant=inset]:rounded-xl peer-data-[variant=inset]:ring peer-data-[variant=inset]:ring-border peer-data-[variant=inset]:shadow-sm lg:peer-data-[variant=floating]:my-4"
>
<div
class="px-4 flex shrink-0 h-(--ui-header-height) items-center"
:class="[
variant !== 'floating' && 'border-b border-border',
side === 'right' && 'justify-end'
]"
>
<PButton
:icon="side === 'left' ? 'i-lucide-panel-left' : 'i-lucide-panel-right'"
color="neutral"
variant="ghost"
aria-label="Toggle sidebar"
@click="open = !open"
/>
</div>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
</div>
</template>state in the slot props to customize the content of the sidebar when it is collapsed.Side
Use the side prop to change the side of the sidebar. Defaults to left.
<script setup lang="ts">
import type { NavigationMenuItem, SidebarProps } from 'pohon-ui'
// Ignore the props for the example
defineProps<Pick<SidebarProps, 'variant' | 'collapsible' | 'side'>>()
const open = ref(true)
const items: Array<NavigationMenuItem> = [
{
label: 'Home',
icon: 'i-lucide-house',
active: true
},
{
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4'
},
{
label: 'Contacts',
icon: 'i-lucide-users'
}
]
</script>
<template>
<div
class="flex flex-1"
:class="[
variant === 'inset' && 'bg-neutral-50 dark:bg-neutral-950',
side === 'right' && 'flex-row-reverse'
]"
>
<PSidebar
v-model:open="open"
:variant="variant"
:collapsible="collapsible"
:side="side"
:ui="{
container: 'h-full'
}"
>
<template #header>
<PIcon name="i-logos-nuxt-icon" class="size-8" />
</template>
<PNavigationMenu
:items="items"
orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }"
/>
</PSidebar>
<div
class="lg:peer-data-[variant=inset]:not-peer-data-[collapsible=offcanvas]:ms-0 bg-background flex flex-1 flex-col overflow-hidden peer-data-[variant=inset]:m-4 peer-data-[variant=inset]:rounded-xl peer-data-[variant=inset]:ring peer-data-[variant=inset]:ring-border peer-data-[variant=inset]:shadow-sm lg:peer-data-[variant=floating]:my-4"
>
<div
class="px-4 flex shrink-0 h-(--ui-header-height) items-center"
:class="[
variant !== 'floating' && 'border-b border-border',
side === 'right' && 'justify-end'
]"
>
<PButton
:icon="side === 'left' ? 'i-lucide-panel-left' : 'i-lucide-panel-right'"
color="neutral"
variant="ghost"
aria-label="Toggle sidebar"
@click="open = !open"
/>
</div>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
</div>
</template><script setup lang="ts">
import { ref } from 'vue'
import type { NavigationMenuItem, SidebarProps } from 'pohon-ui'
// Ignore the props for the example
defineProps<Pick<SidebarProps, 'variant' | 'collapsible' | 'side'>>()
const open = ref(true)
const items: Array<NavigationMenuItem> = [
{
label: 'Home',
icon: 'i-lucide-house',
active: true
},
{
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4'
},
{
label: 'Contacts',
icon: 'i-lucide-users'
}
]
</script>
<template>
<div
class="flex flex-1"
:class="[
variant === 'inset' && 'bg-neutral-50 dark:bg-neutral-950',
side === 'right' && 'flex-row-reverse'
]"
>
<PSidebar
v-model:open="open"
:variant="variant"
:collapsible="collapsible"
:side="side"
:ui="{
container: 'h-full'
}"
>
<template #header>
<PIcon name="i-logos-nuxt-icon" class="size-8" />
</template>
<PNavigationMenu
:items="items"
orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }"
/>
</PSidebar>
<div
class="lg:peer-data-[variant=inset]:not-peer-data-[collapsible=offcanvas]:ms-0 bg-background flex flex-1 flex-col overflow-hidden peer-data-[variant=inset]:m-4 peer-data-[variant=inset]:rounded-xl peer-data-[variant=inset]:ring peer-data-[variant=inset]:ring-border peer-data-[variant=inset]:shadow-sm lg:peer-data-[variant=floating]:my-4"
>
<div
class="px-4 flex shrink-0 h-(--ui-header-height) items-center"
:class="[
variant !== 'floating' && 'border-b border-border',
side === 'right' && 'justify-end'
]"
>
<PButton
:icon="side === 'left' ? 'i-lucide-panel-left' : 'i-lucide-panel-right'"
color="neutral"
variant="ghost"
aria-label="Toggle sidebar"
@click="open = !open"
/>
</div>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
</div>
</template>Title
Use the title prop to set the title of the sidebar header.
<template>
<PSidebar title="Navigation">
<Placeholder class="h-full" />
</PSidebar>
</template>Description
Use the description prop to set the description of the sidebar header.
<template>
<PSidebar title="Navigation" description="Browse your workspace">
<Placeholder class="h-full" />
</PSidebar>
</template>Rail
Use the rail prop to display a thin interactive edge on the sidebar that toggles the collapsed state on click. The rail is only rendered when collapsible is not none.
<template>
<PSidebar rail collapsible="icon" title="Navigation">
<Placeholder class="h-full" />
</PSidebar>
</template>Close
Use the close prop to display a close button in the sidebar header. The close button is only rendered when collapsible is not none.
You can pass any property from the Button component to customize it.
<template>
<PSidebar close rail collapsible="icon" title="Navigation">
<Placeholder class="h-full" />
</PSidebar>
</template>Close Icon
Use the close-icon prop to customize the close button Icon. Defaults to i-lucide-x.
<template>
<PSidebar
close
close-icon="i-lucide-panel-right-close"
rail
collapsible="icon"
side="right"
title="Navigation"
>
<Placeholder class="h-full" />
</PSidebar>
</template>Breakpoint
Use the breakpoint prop to change the breakpoint below which the sidebar renders as a menu. Defaults to lg.
<template>
<PSidebar breakpoint="xl" />
</template>
Mode
Use the mode prop to change the mode of the sidebar menu on mobile. Defaults to slideover.
<script setup lang="ts">
import type { NavigationMenuItem, SidebarProps } from 'pohon-ui';
// Ignore the props for the example
defineProps<Pick<SidebarProps, 'mode'>>();
const open = ref(true);
const items: Array<NavigationMenuItem> = [{
label: 'Home',
icon: 'i-lucide-house',
active: true,
}, {
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4',
}, {
label: 'Contacts',
icon: 'i-lucide-users',
}];
</script>
<template>
<div class="flex flex-1">
<PSidebar
v-model:open="open"
:mode="mode"
title="Navigation"
>
<PNavigationMenu
:items="items"
orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }"
/>
</PSidebar>
<div class="flex flex-1 flex-col">
<div class="px-4 border-b border-border flex shrink-0 h-(--ui-header-height) items-center">
<PButton
icon="i-lucide-panel-left"
color="neutral"
variant="ghost"
aria-label="Toggle sidebar"
@click="open = !open"
/>
</div>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { NavigationMenuItem, SidebarProps } from 'pohon-ui';
// Ignore the props for the example
defineProps<Pick<SidebarProps, 'mode'>>();
const open = ref(true);
const items: Array<NavigationMenuItem> = [{
label: 'Home',
icon: 'i-lucide-house',
active: true,
}, {
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4',
}, {
label: 'Contacts',
icon: 'i-lucide-users',
}];
</script>
<template>
<div class="flex flex-1">
<PSidebar
v-model:open="open"
:mode="mode"
title="Navigation"
>
<PNavigationMenu
:items="items"
orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }"
/>
</PSidebar>
<div class="flex flex-1 flex-col">
<div class="px-4 border-b border-border flex shrink-0 h-(--ui-header-height) items-center">
<PButton
icon="i-lucide-panel-left"
color="neutral"
variant="ghost"
aria-label="Toggle sidebar"
@click="open = !open"
/>
</div>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
</div>
</template>
menu prop to customize the menu of the sidebar, it will adapt depending on the mode you choose.Examples
Control open state
You can control the open state by using the open prop or the v-model:open directive. On desktop it controls the expanded/collapsed state, on mobile it opens/closes the sheet menu.
<script setup lang="ts">
import type { NavigationMenuItem } from 'pohon-ui'
const open = ref(true)
defineShortcuts({
o: () => (open.value = !open.value)
})
const items: Array<NavigationMenuItem> = [
{
label: 'Home',
icon: 'i-lucide-house',
active: true
},
{
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4'
},
{
label: 'Contacts',
icon: 'i-lucide-users'
}
]
</script>
<template>
<div class="flex flex-1">
<PSidebar v-model:open="open" title="Navigation" collapsible="icon">
<PNavigationMenu
:items="items"
orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }"
/>
</PSidebar>
<div class="flex flex-1 flex-col">
<div class="px-4 border-b border-border flex shrink-0 h-(--ui-header-height) items-center">
<PButton
icon="i-lucide-panel-left"
color="neutral"
variant="ghost"
:aria-label="open ? 'Close sidebar' : 'Open sidebar'"
@click="open = !open"
/>
</div>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
</div>
</template><script setup lang="ts">
import { ref } from 'vue'
import type { NavigationMenuItem } from 'pohon-ui'
const open = ref(true)
defineShortcuts({
o: () => (open.value = !open.value)
})
const items: Array<NavigationMenuItem> = [
{
label: 'Home',
icon: 'i-lucide-house',
active: true
},
{
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4'
},
{
label: 'Contacts',
icon: 'i-lucide-users'
}
]
</script>
<template>
<div class="flex flex-1">
<PSidebar v-model:open="open" title="Navigation" collapsible="icon">
<PNavigationMenu
:items="items"
orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }"
/>
</PSidebar>
<div class="flex flex-1 flex-col">
<div class="px-4 border-b border-border flex shrink-0 h-(--ui-header-height) items-center">
<PButton
icon="i-lucide-panel-left"
color="neutral"
variant="ghost"
:aria-label="open ? 'Close sidebar' : 'Open sidebar'"
@click="open = !open"
/>
</div>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
</div>
</template>defineShortcuts, you can toggle the open state of the Sidebar by pressing O.Persist open state
Use useLocalStorage from VueUse or useCookie instead of ref to persist the sidebar state across page reloads.
<script setup lang="ts">
import type { NavigationMenuItem } from 'pohon-ui'
const open = useLocalStorage('sidebar-open', true)
defineShortcuts({
o: () => (open.value = !open.value)
})
const items: Array<NavigationMenuItem> = [
{
label: 'Home',
icon: 'i-lucide-house',
active: true
},
{
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4'
},
{
label: 'Contacts',
icon: 'i-lucide-users'
}
]
</script>
<template>
<div class="flex flex-1">
<PSidebar v-model:open="open" title="Navigation" collapsible="icon">
<PNavigationMenu
:items="items"
orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }"
/>
</PSidebar>
<div class="flex flex-1 flex-col">
<div class="px-4 border-b border-border flex shrink-0 h-(--ui-header-height) items-center">
<PButton
icon="i-lucide-panel-left"
color="neutral"
variant="ghost"
aria-label="Toggle sidebar"
@click="open = !open"
/>
</div>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
</div>
</template><script setup lang="ts">
import { useLocalStorage } from '@vueuse/core'
import type { NavigationMenuItem } from 'pohon-ui'
const open = useLocalStorage('sidebar-open', true)
defineShortcuts({
o: () => (open.value = !open.value)
})
const items: Array<NavigationMenuItem> = [
{
label: 'Home',
icon: 'i-lucide-house',
active: true
},
{
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4'
},
{
label: 'Contacts',
icon: 'i-lucide-users'
}
]
</script>
<template>
<div class="flex flex-1">
<PSidebar v-model:open="open" title="Navigation" collapsible="icon">
<PNavigationMenu
:items="items"
orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }"
/>
</PSidebar>
<div class="flex flex-1 flex-col">
<div class="px-4 border-b border-border flex shrink-0 h-(--ui-header-height) items-center">
<PButton
icon="i-lucide-panel-left"
color="neutral"
variant="ghost"
aria-label="Toggle sidebar"
@click="open = !open"
/>
</div>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
</div>
</template>ref(true) with useLocalStorage('sidebar-open', true).With custom width
The sidebar width is controlled by the --sidebar-width CSS variable (defaults to 16rem). The collapsed icon width is controlled by --sidebar-width-icon (defaults to 4rem).
Override them globally in your CSS or per-instance with the style attribute.
<script setup lang="ts">
import type { NavigationMenuItem } from 'pohon-ui'
const open = ref(true)
const items: Array<NavigationMenuItem> = [
{
label: 'Home',
icon: 'i-lucide-house',
active: true
},
{
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4'
},
{
label: 'Contacts',
icon: 'i-lucide-users'
}
]
</script>
<template>
<div class="flex flex-1">
<PSidebar v-model:open="open" collapsible="icon" :style="{ '--sidebar-width': '20rem' }">
<PNavigationMenu
:items="items"
orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }"
/>
</PSidebar>
<div class="flex flex-1 flex-col">
<div class="px-4 border-b border-border flex shrink-0 h-(--ui-header-height) items-center">
<PButton
icon="i-lucide-panel-left"
color="neutral"
variant="ghost"
aria-label="Toggle sidebar"
@click="open = !open"
/>
</div>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
</div>
</template><script setup lang="ts">
import { ref } from 'vue'
import type { NavigationMenuItem } from 'pohon-ui'
const open = ref(true)
const items: Array<NavigationMenuItem> = [
{
label: 'Home',
icon: 'i-lucide-house',
active: true
},
{
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4'
},
{
label: 'Contacts',
icon: 'i-lucide-users'
}
]
</script>
<template>
<div class="flex flex-1">
<PSidebar v-model:open="open" collapsible="icon" :style="{ '--sidebar-width': '20rem' }">
<PNavigationMenu
:items="items"
orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }"
/>
</PSidebar>
<div class="flex flex-1 flex-col">
<div class="px-4 border-b border-border flex shrink-0 h-(--ui-header-height) items-center">
<PButton
icon="i-lucide-panel-left"
color="neutral"
variant="ghost"
aria-label="Toggle sidebar"
@click="open = !open"
/>
</div>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
</div>
</template>With header
To position the sidebar below a Header, customize the gap and container using the ui prop.
<script setup lang="ts">
import type { NavigationMenuItem } from 'pohon-ui'
const open = ref(true)
const items: Array<NavigationMenuItem> = [
{
label: 'Home',
icon: 'i-lucide-house',
active: true
},
{
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4'
},
{
label: 'Contacts',
icon: 'i-lucide-users'
}
]
</script>
<template>
<div class="flex flex-1 flex-col">
<PHeader toggle-side="left" :ui="{ container: 'px-4!' }">
<template #toggle>
<PButton
icon="i-lucide-panel-left"
color="neutral"
variant="ghost"
aria-label="Toggle sidebar"
@click="open = !open"
/>
</template>
</PHeader>
<div class="flex flex-1 min-h-0">
<PSidebar
v-model:open="open"
collapsible="icon"
:ui="{
gap: 'h-[calc(100%-var(--ui-header-height))]',
container:
'absolute top-$ui-header-height bottom-0 h-[calc(100%-var(--ui-header-height))]'
}"
>
<PNavigationMenu
:items="items"
orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }"
/>
</PSidebar>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
</div>
</template><script setup lang="ts">
import { ref } from 'vue'
import type { NavigationMenuItem } from 'pohon-ui'
const open = ref(true)
const items: Array<NavigationMenuItem> = [
{
label: 'Home',
icon: 'i-lucide-house',
active: true
},
{
label: 'Inbox',
icon: 'i-lucide-inbox',
badge: '4'
},
{
label: 'Contacts',
icon: 'i-lucide-users'
}
]
</script>
<template>
<div class="flex flex-1 flex-col">
<PHeader toggle-side="left" :ui="{ container: 'px-4!' }">
<template #toggle>
<PButton
icon="i-lucide-panel-left"
color="neutral"
variant="ghost"
aria-label="Toggle sidebar"
@click="open = !open"
/>
</template>
</PHeader>
<div class="flex flex-1 min-h-0">
<PSidebar
v-model:open="open"
collapsible="icon"
:ui="{
gap: 'h-[calc(100%-var(--ui-header-height))]',
container:
'absolute top-$ui-header-height bottom-0 h-[calc(100%-var(--ui-header-height))]'
}"
>
<PNavigationMenu
:items="items"
orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }"
/>
</PSidebar>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
</div>
</template>--ui-header-height variable defaults to 4rem and is used by the Header. Adjust it if your navbar uses a different height.With AI chat
Use the sidebar on the right side with ChatMessages and ChatPrompt to create an AI chat panel.
<script setup lang="ts">
import type { UIMessage } from 'ai'
import { useChat } from '@ai-sdk/vue'
import { Markdown } from '@comark/vue'
import security from '@comark/vue/plugins/security'
import shiki from '@comark/vue/plugins/shiki'
import { isTextUIPart } from 'ai'
import { isPartStreaming } from 'pohon-ui/utils/ai'
const plugins = [
shiki(),
security({ blockedTags: ['script', 'style', 'iframe', 'object', 'embed', 'form'] })
]
const open = ref(true)
const input = ref('')
const initialMessages: Array<UIMessage> = [
{
id: '1',
role: 'user',
parts: [{ type: 'text', text: 'What is Pohon UI?' }]
},
{
id: '2',
role: 'assistant',
parts: [
{
type: 'text',
text: 'Pohon UI is a Vue component library built on Akar, UnoCSS, and UnoCSS Variants. It provides 125+ accessible components for building modern web apps.'
}
]
}
]
const { messages, status, error, sendMessage, regenerate, stop } = useChat({
messages: initialMessages
})
function onSubmit() {
if (!input.value.trim()) {
return
}
sendMessage({ text: input.value })
input.value = ''
}
const ui = {
prose: {
p: { base: 'my-2 text-sm/6' },
li: { base: 'my-0.5 text-sm/6' },
ul: { base: 'my-2' },
ol: { base: 'my-2' },
h1: { base: 'text-xl mb-4' },
h2: { base: 'text-lg mt-6 mb-3' },
h3: { base: 'text-base mt-4 mb-2' },
h4: { base: 'text-sm mt-3 mb-1.5' },
code: { base: 'text-xs' },
pre: { root: 'my-2', base: 'text-xs/5' },
table: { root: 'my-2' },
hr: { base: 'my-4' }
}
}
</script>
<template>
<div class="flex flex-1">
<div class="flex flex-1 flex-col">
<div
class="px-4 border-b border-border flex shrink-0 h-(--ui-header-height) items-center justify-end"
>
<PButton
icon="i-lucide-panel-right"
color="neutral"
variant="ghost"
aria-label="Toggle sidebar"
@click="open = !open"
/>
</div>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
<PSidebar
v-model:open="open"
side="right"
title="AI Chat"
close
:style="{ '--sidebar-width': '20rem' }"
:ui="{ container: 'h-full' }"
>
<PTheme :ui="ui">
<PChatMessages :messages="messages" :status="status" compact class="px-0">
<template #content="{ message }">
<template
v-for="(part, index) in message.parts"
:key="`${message.id}-${part.type}-${index}`"
>
<template v-if="isTextUIPart(part)">
<Markdown
v-if="message.role === 'assistant'"
:value="part.text"
:streaming="isPartStreaming(part)"
:plugins="plugins"
class="*:first:mt-0 *:last:mb-0"
/>
<p v-else-if="message.role === 'user'" class="text-sm/6 whitespace-pre-wrap">
{{ part.text }}
</p>
</template>
</template>
</template>
</PChatMessages>
</PTheme>
<template #footer>
<PChatPrompt
v-model="input"
:error="error"
:autofocus="false"
variant="subtle"
size="sm"
@submit="onSubmit"
>
<PChatPromptSubmit size="sm" :status="status" @stop="stop()" @reload="regenerate()" />
</PChatPrompt>
</template>
</PSidebar>
</div>
</template><script setup lang="ts">
import { ref } from 'vue'
import type { UIMessage } from 'ai'
import { useChat } from '@ai-sdk/vue'
import { Markdown } from '@comark/vue'
import security from '@comark/vue/plugins/security'
import shiki from '@comark/vue/plugins/shiki'
import { isTextUIPart } from 'ai'
import { isPartStreaming } from 'pohon-ui/utils/ai'
const plugins = [
shiki(),
security({ blockedTags: ['script', 'style', 'iframe', 'object', 'embed', 'form'] })
]
const open = ref(true)
const input = ref('')
const initialMessages: Array<UIMessage> = [
{
id: '1',
role: 'user',
parts: [{ type: 'text', text: 'What is Pohon UI?' }]
},
{
id: '2',
role: 'assistant',
parts: [
{
type: 'text',
text: 'Pohon UI is a Vue component library built on Akar, UnoCSS, and UnoCSS Variants. It provides 125+ accessible components for building modern web apps.'
}
]
}
]
const { messages, status, error, sendMessage, regenerate, stop } = useChat({
messages: initialMessages
})
function onSubmit() {
if (!input.value.trim()) {
return
}
sendMessage({ text: input.value })
input.value = ''
}
const ui = {
prose: {
p: { base: 'my-2 text-sm/6' },
li: { base: 'my-0.5 text-sm/6' },
ul: { base: 'my-2' },
ol: { base: 'my-2' },
h1: { base: 'text-xl mb-4' },
h2: { base: 'text-lg mt-6 mb-3' },
h3: { base: 'text-base mt-4 mb-2' },
h4: { base: 'text-sm mt-3 mb-1.5' },
code: { base: 'text-xs' },
pre: { root: 'my-2', base: 'text-xs/5' },
table: { root: 'my-2' },
hr: { base: 'my-4' }
}
}
</script>
<template>
<div class="flex flex-1">
<div class="flex flex-1 flex-col">
<div
class="px-4 border-b border-border flex shrink-0 h-(--ui-header-height) items-center justify-end"
>
<PButton
icon="i-lucide-panel-right"
color="neutral"
variant="ghost"
aria-label="Toggle sidebar"
@click="open = !open"
/>
</div>
<div class="p-4 flex-1">
<Placeholder class="size-full" />
</div>
</div>
<PSidebar
v-model:open="open"
side="right"
title="AI Chat"
close
:style="{ '--sidebar-width': '20rem' }"
:ui="{ container: 'h-full' }"
>
<PTheme :ui="ui">
<PChatMessages :messages="messages" :status="status" compact class="px-0">
<template #content="{ message }">
<template
v-for="(part, index) in message.parts"
:key="`${message.id}-${part.type}-${index}`"
>
<template v-if="isTextUIPart(part)">
<Markdown
v-if="message.role === 'assistant'"
:value="part.text"
:streaming="isPartStreaming(part)"
:plugins="plugins"
class="*:first:mt-0 *:last:mb-0"
/>
<p v-else-if="message.role === 'user'" class="text-sm/6 whitespace-pre-wrap">
{{ part.text }}
</p>
</template>
</template>
</template>
</PChatMessages>
</PTheme>
<template #footer>
<PChatPrompt
v-model="input"
:error="error"
:autofocus="false"
variant="subtle"
size="sm"
@submit="onSubmit"
>
<PChatPromptSubmit size="sm" :status="status" @stop="stop()" @reload="regenerate()" />
</PChatPrompt>
</template>
</PSidebar>
</div>
</template>API
Props
| Prop | Default | Type |
|---|
Slots
| Slot | Type |
|---|
Theme
export default defineAppConfig({
ui: {
sidebar: {
slots: {
root: 'peer [--sidebar-width:16rem] [--sidebar-width-icon:4rem]',
gap: 'bg-transparent w-$sidebar-width relative',
container: 'w-$sidebar-width hidden inset-y-0 fixed z-10 h-svh',
inner: 'flex size-full flex-col overflow-hidden divide-y divide-border',
header: 'px-4 flex gap-1.5 min-h-$ui-header-height items-center overflow-hidden',
wrapper: 'min-w-0 flex-1',
title: 'color-text-highlighted font-600 truncate',
description: 'color-text-muted text-sm truncate',
actions: 'flex items-center gap-1.5 shrink-0',
close: '',
body: 'flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4',
footer: 'flex items-center gap-1.5 overflow-hidden p-4',
rail: [
'w-4 hidden inset-y-0 absolute z-20 after:w-px after:content-empty after:inset-y-0 after:left-1/2 after:absolute hover:after:bg-$ui-color-border-accented',
'after:transition-colors'
]
},
variants: {
transition: {
true: {
gap: 'transition-[width]-280 ease-out motion-reduce:transition-none',
container: 'duration-280 ease-out [transition-property:inset-inline-start,inset-inline-end,width] motion-reduce:transition-none',
rail: 'transition-all ease-out'
}
},
breakpoint: {
sm: {
container: 'sm:flex',
rail: 'sm:flex'
},
md: {
container: 'md:flex',
rail: 'md:flex'
},
lg: {
container: 'lg:flex',
rail: 'lg:flex'
},
xl: {
container: 'xl:flex',
rail: 'xl:flex'
},
'2xl': {
container: '2xl:flex',
rail: '2xl:flex'
}
},
side: {
left: {
container: 'border-e border-border start-0',
rail: 'translate-x-1/2 end-0 rtl:-translate-x-1/2'
},
right: {
container: 'border-s border-border end-0',
rail: '-translate-x-1/2 rtl:translate-x-1/2 -start-px'
}
},
collapsible: {
offcanvas: {
root: 'group/sidebar hidden',
gap: 'data-[state=collapsed]:w-0'
},
icon: {
root: 'group/sidebar hidden',
gap: 'data-[state=collapsed]:w-$sidebar-width-icon',
container: 'data-[state=collapsed]:w-$sidebar-width-icon',
actions: 'group-data-[state=collapsed]/sidebar:hidden',
body: 'group-data-[state=collapsed]/sidebar:overflow-hidden'
},
none: {
root: 'h-full w-$sidebar-width'
}
},
variant: {
sidebar: {},
floating: {
container: 'p-4 border-transparent',
inner: 'rounded-lg ring ring-border shadow-lg',
rail: 'inset-y-4'
},
inset: {
container: 'py-4 border-transparent',
inner: 'divide-transparent',
rail: 'inset-y-4'
}
}
},
compoundVariants: [
{
breakpoint: 'sm',
collapsible: [
'offcanvas',
'icon'
],
class: {
root: 'sm:block'
}
},
{
breakpoint: 'md',
collapsible: [
'offcanvas',
'icon'
],
class: {
root: 'md:block'
}
},
{
breakpoint: 'lg',
collapsible: [
'offcanvas',
'icon'
],
class: {
root: 'lg:block'
}
},
{
breakpoint: 'xl',
collapsible: [
'offcanvas',
'icon'
],
class: {
root: 'xl:block'
}
},
{
breakpoint: '2xl',
collapsible: [
'offcanvas',
'icon'
],
class: {
root: '2xl:block'
}
},
{
side: 'left',
collapsible: [
'offcanvas',
'icon'
],
class: {
rail: 'cursor-w-resize rtl:cursor-e-resize data-[state=collapsed]:cursor-e-resize data-[state=collapsed]:rtl:cursor-w-resize'
}
},
{
side: 'right',
collapsible: [
'offcanvas',
'icon'
],
class: {
rail: 'cursor-e-resize rtl:cursor-w-resize data-[state=collapsed]:cursor-w-resize data-[state=collapsed]:rtl:cursor-e-resize'
}
},
{
side: 'left',
collapsible: 'none',
class: {
root: 'border-e border-border'
}
},
{
side: 'right',
collapsible: 'none',
class: {
root: 'border-s border-border'
}
},
{
side: 'left',
collapsible: 'offcanvas',
class: {
container: 'data-[state=collapsed]:-start-$sidebar-width'
}
},
{
side: 'right',
collapsible: 'offcanvas',
class: {
container: 'data-[state=collapsed]:-end-$sidebar-width'
}
},
{
variant: 'floating',
collapsible: 'icon',
class: {
gap: 'data-[state=collapsed]:w-[calc(var(--sidebar-width-icon)+var(--spacing)*8)]',
container: 'data-[state=collapsed]:w-[calc(var(--sidebar-width-icon)+var(--spacing)*8+2px)]'
}
},
{
variant: 'floating',
collapsible: 'none',
class: {
root: 'p-4 border-0'
}
},
{
variant: 'inset',
collapsible: 'none',
class: {
root: 'py-4 border-0'
}
},
{
variant: 'floating',
side: 'left',
class: {
rail: 'end-4'
}
},
{
variant: 'floating',
side: 'right',
class: {
rail: 'start-[calc(var(--spacing)*4-1px)]'
}
}
]
}
}
})import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from 'pohon-ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
sidebar: {
slots: {
root: 'peer [--sidebar-width:16rem] [--sidebar-width-icon:4rem]',
gap: 'bg-transparent w-$sidebar-width relative',
container: 'w-$sidebar-width hidden inset-y-0 fixed z-10 h-svh',
inner: 'flex size-full flex-col overflow-hidden divide-y divide-border',
header: 'px-4 flex gap-1.5 min-h-$ui-header-height items-center overflow-hidden',
wrapper: 'min-w-0 flex-1',
title: 'color-text-highlighted font-600 truncate',
description: 'color-text-muted text-sm truncate',
actions: 'flex items-center gap-1.5 shrink-0',
close: '',
body: 'flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4',
footer: 'flex items-center gap-1.5 overflow-hidden p-4',
rail: [
'w-4 hidden inset-y-0 absolute z-20 after:w-px after:content-empty after:inset-y-0 after:left-1/2 after:absolute hover:after:bg-$ui-color-border-accented',
'after:transition-colors'
]
},
variants: {
transition: {
true: {
gap: 'transition-[width]-280 ease-out motion-reduce:transition-none',
container: 'duration-280 ease-out [transition-property:inset-inline-start,inset-inline-end,width] motion-reduce:transition-none',
rail: 'transition-all ease-out'
}
},
breakpoint: {
sm: {
container: 'sm:flex',
rail: 'sm:flex'
},
md: {
container: 'md:flex',
rail: 'md:flex'
},
lg: {
container: 'lg:flex',
rail: 'lg:flex'
},
xl: {
container: 'xl:flex',
rail: 'xl:flex'
},
'2xl': {
container: '2xl:flex',
rail: '2xl:flex'
}
},
side: {
left: {
container: 'border-e border-border start-0',
rail: 'translate-x-1/2 end-0 rtl:-translate-x-1/2'
},
right: {
container: 'border-s border-border end-0',
rail: '-translate-x-1/2 rtl:translate-x-1/2 -start-px'
}
},
collapsible: {
offcanvas: {
root: 'group/sidebar hidden',
gap: 'data-[state=collapsed]:w-0'
},
icon: {
root: 'group/sidebar hidden',
gap: 'data-[state=collapsed]:w-$sidebar-width-icon',
container: 'data-[state=collapsed]:w-$sidebar-width-icon',
actions: 'group-data-[state=collapsed]/sidebar:hidden',
body: 'group-data-[state=collapsed]/sidebar:overflow-hidden'
},
none: {
root: 'h-full w-$sidebar-width'
}
},
variant: {
sidebar: {},
floating: {
container: 'p-4 border-transparent',
inner: 'rounded-lg ring ring-border shadow-lg',
rail: 'inset-y-4'
},
inset: {
container: 'py-4 border-transparent',
inner: 'divide-transparent',
rail: 'inset-y-4'
}
}
},
compoundVariants: [
{
breakpoint: 'sm',
collapsible: [
'offcanvas',
'icon'
],
class: {
root: 'sm:block'
}
},
{
breakpoint: 'md',
collapsible: [
'offcanvas',
'icon'
],
class: {
root: 'md:block'
}
},
{
breakpoint: 'lg',
collapsible: [
'offcanvas',
'icon'
],
class: {
root: 'lg:block'
}
},
{
breakpoint: 'xl',
collapsible: [
'offcanvas',
'icon'
],
class: {
root: 'xl:block'
}
},
{
breakpoint: '2xl',
collapsible: [
'offcanvas',
'icon'
],
class: {
root: '2xl:block'
}
},
{
side: 'left',
collapsible: [
'offcanvas',
'icon'
],
class: {
rail: 'cursor-w-resize rtl:cursor-e-resize data-[state=collapsed]:cursor-e-resize data-[state=collapsed]:rtl:cursor-w-resize'
}
},
{
side: 'right',
collapsible: [
'offcanvas',
'icon'
],
class: {
rail: 'cursor-e-resize rtl:cursor-w-resize data-[state=collapsed]:cursor-w-resize data-[state=collapsed]:rtl:cursor-e-resize'
}
},
{
side: 'left',
collapsible: 'none',
class: {
root: 'border-e border-border'
}
},
{
side: 'right',
collapsible: 'none',
class: {
root: 'border-s border-border'
}
},
{
side: 'left',
collapsible: 'offcanvas',
class: {
container: 'data-[state=collapsed]:-start-$sidebar-width'
}
},
{
side: 'right',
collapsible: 'offcanvas',
class: {
container: 'data-[state=collapsed]:-end-$sidebar-width'
}
},
{
variant: 'floating',
collapsible: 'icon',
class: {
gap: 'data-[state=collapsed]:w-[calc(var(--sidebar-width-icon)+var(--spacing)*8)]',
container: 'data-[state=collapsed]:w-[calc(var(--sidebar-width-icon)+var(--spacing)*8+2px)]'
}
},
{
variant: 'floating',
collapsible: 'none',
class: {
root: 'p-4 border-0'
}
},
{
variant: 'inset',
collapsible: 'none',
class: {
root: 'py-4 border-0'
}
},
{
variant: 'floating',
side: 'left',
class: {
rail: 'end-4'
}
},
{
variant: 'floating',
side: 'right',
class: {
rail: 'start-[calc(var(--spacing)*4-1px)]'
}
}
]
}
}
})
]
})