DropdownMenu
Usage
Use a Button or any other component in the default slot of the DropdownMenu.
<script setup lang="ts">
import type { DropdownMenuItem } from 'pohon-ui'
const items = ref<DropdownMenuItem[][]>([
[
{
label: 'Benjamin',
avatar: {
src: 'https://github.com/benjamincanac.png',
loading: 'lazy'
},
type: 'label'
}
],
[
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-cog',
kbds: [',']
},
{
label: 'Keyboard shortcuts',
icon: 'i-lucide-monitor'
}
],
[
{
label: 'Team',
icon: 'i-lucide-users',
filter: {
placeholder: 'Search members...'
},
children: [
[
{
label: 'benjamincanac',
avatar: {
src: 'https://github.com/benjamincanac.png',
loading: 'lazy'
}
},
{
label: 'HugoRCD',
avatar: {
src: 'https://github.com/HugoRCD.png',
loading: 'lazy'
}
},
{
label: 'atinux',
avatar: {
src: 'https://github.com/atinux.png',
loading: 'lazy'
}
},
{
label: 'romhml',
avatar: {
src: 'https://github.com/romhml.png',
loading: 'lazy'
}
},
{
label: 'sandros94',
avatar: {
src: 'https://github.com/sandros94.png',
loading: 'lazy'
}
},
{
label: 'J-Michalek',
avatar: {
src: 'https://github.com/J-Michalek.png',
loading: 'lazy'
}
},
{
label: 'hywax',
avatar: {
src: 'https://github.com/hywax.png',
loading: 'lazy'
}
}
]
]
},
{
label: 'Invite users',
icon: 'i-lucide-user-plus',
children: [
[
{
label: 'Email',
icon: 'i-lucide-mail'
},
{
label: 'Message',
icon: 'i-lucide-message-square'
}
],
[
{
label: 'More',
icon: 'i-lucide-circle-plus',
children: [
{
label: 'Import from Slack',
icon: 'i-simple-icons-slack',
to: 'https://slack.com',
target: '_blank'
},
{
label: 'Import from Trello',
icon: 'i-simple-icons-trello'
},
{
label: 'Import from Asana',
icon: 'i-simple-icons-asana'
}
]
}
]
]
},
{
label: 'New team',
icon: 'i-lucide-plus',
kbds: ['meta', 'n']
}
],
[
{
label: 'GitHub',
icon: 'i-simple-icons-github',
to: 'https://github.com/vinicunca/pohon',
target: '_blank'
},
{
label: 'Support',
icon: 'i-lucide-life-buoy',
to: '/pohon/components/dropdown-menu'
},
{
label: 'API',
icon: 'i-lucide-cloud',
disabled: true
}
],
[
{
label: 'Logout',
icon: 'i-lucide-log-out',
color: 'error',
kbds: ['shift', 'meta', 'q']
}
]
])
</script>
<template>
<PDropdownMenu :items="items">
<PButton icon="i-lucide-menu" color="neutral" variant="outline" />
</PDropdownMenu>
</template><script setup lang="ts">
import { ref } from 'vue'
import type { DropdownMenuItem } from 'pohon-ui'
const items = ref<DropdownMenuItem[][]>([
[
{
label: 'Benjamin',
avatar: {
src: 'https://github.com/benjamincanac.png',
loading: 'lazy'
},
type: 'label'
}
],
[
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-cog',
kbds: [',']
},
{
label: 'Keyboard shortcuts',
icon: 'i-lucide-monitor'
}
],
[
{
label: 'Team',
icon: 'i-lucide-users',
filter: {
placeholder: 'Search members...'
},
children: [
[
{
label: 'benjamincanac',
avatar: {
src: 'https://github.com/benjamincanac.png',
loading: 'lazy'
}
},
{
label: 'HugoRCD',
avatar: {
src: 'https://github.com/HugoRCD.png',
loading: 'lazy'
}
},
{
label: 'atinux',
avatar: {
src: 'https://github.com/atinux.png',
loading: 'lazy'
}
},
{
label: 'romhml',
avatar: {
src: 'https://github.com/romhml.png',
loading: 'lazy'
}
},
{
label: 'sandros94',
avatar: {
src: 'https://github.com/sandros94.png',
loading: 'lazy'
}
},
{
label: 'J-Michalek',
avatar: {
src: 'https://github.com/J-Michalek.png',
loading: 'lazy'
}
},
{
label: 'hywax',
avatar: {
src: 'https://github.com/hywax.png',
loading: 'lazy'
}
}
]
]
},
{
label: 'Invite users',
icon: 'i-lucide-user-plus',
children: [
[
{
label: 'Email',
icon: 'i-lucide-mail'
},
{
label: 'Message',
icon: 'i-lucide-message-square'
}
],
[
{
label: 'More',
icon: 'i-lucide-circle-plus',
children: [
{
label: 'Import from Slack',
icon: 'i-simple-icons-slack',
to: 'https://slack.com',
target: '_blank'
},
{
label: 'Import from Trello',
icon: 'i-simple-icons-trello'
},
{
label: 'Import from Asana',
icon: 'i-simple-icons-asana'
}
]
}
]
]
},
{
label: 'New team',
icon: 'i-lucide-plus',
kbds: ['meta', 'n']
}
],
[
{
label: 'GitHub',
icon: 'i-simple-icons-github',
to: 'https://github.com/vinicunca/pohon',
target: '_blank'
},
{
label: 'Support',
icon: 'i-lucide-life-buoy',
to: '/pohon/components/dropdown-menu'
},
{
label: 'API',
icon: 'i-lucide-cloud',
disabled: true
}
],
[
{
label: 'Logout',
icon: 'i-lucide-log-out',
color: 'error',
kbds: ['shift', 'meta', 'q']
}
]
])
</script>
<template>
<PDropdownMenu :items="items">
<PButton icon="i-lucide-menu" color="neutral" variant="outline" />
</PDropdownMenu>
</template>Items
Use the items prop as an array of objects with the following properties:
label?: stringicon?: stringavatar?: AvatarPropskbds?: string[] | KbdProps[]type?: "link" | "label" | "separator" | "checkbox"color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"checked?: booleandisabled?: booleanslot?: stringonSelect?: (e: Event) => voidonUpdateChecked?: (checked: boolean) => voidchildren?: DropdownMenuItem[] | DropdownMenuItem[][]filter?: boolean | InputPropsfilterFields?: string[]ignoreFilter?: booleanclass?: anyui?: { item?: ClassNameValue, label?: ClassNameValue, separator?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLabel?: ClassNameValue, itemLabelExternalIcon?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue, itemTrailingKbds?: ClassNameValue, itemTrailingKbdsSize?: ClassNameValue }
You can pass any property from the Link component such as to, target, etc.
<script setup lang="ts">
import type { DropdownMenuItem } from 'pohon-ui'
const items = ref<DropdownMenuItem[][]>([
[
{
label: 'Benjamin',
avatar: {
src: 'https://github.com/benjamincanac.png',
loading: 'lazy'
},
type: 'label'
}
],
[
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-cog',
kbds: [',']
},
{
label: 'Keyboard shortcuts',
icon: 'i-lucide-monitor'
}
],
[
{
label: 'Team',
icon: 'i-lucide-users'
},
{
label: 'Invite users',
icon: 'i-lucide-user-plus',
children: [
[
{
label: 'Email',
icon: 'i-lucide-mail'
},
{
label: 'Message',
icon: 'i-lucide-message-square'
}
],
[
{
label: 'More',
icon: 'i-lucide-circle-plus',
children: [
{
label: 'Import from Slack',
icon: 'i-simple-icons-slack',
to: 'https://slack.com',
target: '_blank'
},
{
label: 'Import from Trello',
icon: 'i-simple-icons-trello'
},
{
label: 'Import from Asana',
icon: 'i-simple-icons-asana'
}
]
}
]
]
},
{
label: 'New team',
icon: 'i-lucide-plus',
kbds: ['meta', 'n']
}
],
[
{
label: 'GitHub',
icon: 'i-simple-icons-github',
to: 'https://github.com/vinicunca/pohon',
target: '_blank'
},
{
label: 'Support',
icon: 'i-lucide-life-buoy',
to: '/pohon/components/dropdown-menu'
},
{
label: 'API',
icon: 'i-lucide-cloud',
disabled: true
}
],
[
{
label: 'Logout',
icon: 'i-lucide-log-out',
kbds: ['shift', 'meta', 'q']
}
]
])
</script>
<template>
<PDropdownMenu
:items="items"
:ui="{
content: 'w-48'
}"
>
<PButton icon="i-lucide-menu" color="neutral" variant="outline" />
</PDropdownMenu>
</template><script setup lang="ts">
import { ref } from 'vue'
import type { DropdownMenuItem } from 'pohon-ui'
const items = ref<DropdownMenuItem[][]>([
[
{
label: 'Benjamin',
avatar: {
src: 'https://github.com/benjamincanac.png',
loading: 'lazy'
},
type: 'label'
}
],
[
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-cog',
kbds: [',']
},
{
label: 'Keyboard shortcuts',
icon: 'i-lucide-monitor'
}
],
[
{
label: 'Team',
icon: 'i-lucide-users'
},
{
label: 'Invite users',
icon: 'i-lucide-user-plus',
children: [
[
{
label: 'Email',
icon: 'i-lucide-mail'
},
{
label: 'Message',
icon: 'i-lucide-message-square'
}
],
[
{
label: 'More',
icon: 'i-lucide-circle-plus',
children: [
{
label: 'Import from Slack',
icon: 'i-simple-icons-slack',
to: 'https://slack.com',
target: '_blank'
},
{
label: 'Import from Trello',
icon: 'i-simple-icons-trello'
},
{
label: 'Import from Asana',
icon: 'i-simple-icons-asana'
}
]
}
]
]
},
{
label: 'New team',
icon: 'i-lucide-plus',
kbds: ['meta', 'n']
}
],
[
{
label: 'GitHub',
icon: 'i-simple-icons-github',
to: 'https://github.com/vinicunca/pohon',
target: '_blank'
},
{
label: 'Support',
icon: 'i-lucide-life-buoy',
to: '/pohon/components/dropdown-menu'
},
{
label: 'API',
icon: 'i-lucide-cloud',
disabled: true
}
],
[
{
label: 'Logout',
icon: 'i-lucide-log-out',
kbds: ['shift', 'meta', 'q']
}
]
])
</script>
<template>
<PDropdownMenu
:items="items"
:ui="{
content: 'w-48'
}"
>
<PButton icon="i-lucide-menu" color="neutral" variant="outline" />
</PDropdownMenu>
</template>items prop to create separated groups of items.children array of objects with the same properties as the items prop to create a nested menu which can be controlled using the open, defaultOpen and content properties.Content
Use the content prop to control how the DropdownMenu content is rendered, like its align or side for example.
<script setup lang="ts">
import type { DropdownMenuItem } from 'pohon-ui'
const items = ref<DropdownMenuItem[]>([
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-cog'
}
])
</script>
<template>
<PDropdownMenu
:items="items"
:content="{
align: 'start',
side: 'bottom',
sideOffset: 8
}"
:ui="{
content: 'w-48'
}"
>
<PButton label="Open" icon="i-lucide-menu" color="neutral" variant="outline" />
</PDropdownMenu>
</template><script setup lang="ts">
import { ref } from 'vue'
import type { DropdownMenuItem } from 'pohon-ui'
const items = ref<DropdownMenuItem[]>([
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-cog'
}
])
</script>
<template>
<PDropdownMenu
:items="items"
:content="{
align: 'start',
side: 'bottom',
sideOffset: 8
}"
:ui="{
content: 'w-48'
}"
>
<PButton label="Open" icon="i-lucide-menu" color="neutral" variant="outline" />
</PDropdownMenu>
</template>Filter
Use the filter prop to display a filter input inside the DropdownMenu. Defaults to false.
filter-fields prop to specify which fields to filter by. By default, it uses the labelKey prop.You can pass any property from the Input component to customize it.
<script setup lang="ts">
import type { DropdownMenuItem } from 'pohon-ui'
const items = ref<DropdownMenuItem[]>([
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-cog'
},
{
label: 'Team',
icon: 'i-lucide-users'
},
{
label: 'Invite users',
icon: 'i-lucide-user-plus'
},
{
label: 'New team',
icon: 'i-lucide-plus'
}
])
</script>
<template>
<PDropdownMenu
:filter="{
icon: 'i-lucide-search'
}"
:items="items"
:content="{
align: 'start'
}"
:ui="{
content: 'w-48'
}"
>
<PButton label="Open" icon="i-lucide-menu" color="neutral" variant="outline" />
</PDropdownMenu>
</template><script setup lang="ts">
import { ref } from 'vue'
import type { DropdownMenuItem } from 'pohon-ui'
const items = ref<DropdownMenuItem[]>([
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-cog'
},
{
label: 'Team',
icon: 'i-lucide-users'
},
{
label: 'Invite users',
icon: 'i-lucide-user-plus'
},
{
label: 'New team',
icon: 'i-lucide-plus'
}
])
</script>
<template>
<PDropdownMenu
:filter="{
icon: 'i-lucide-search'
}"
:items="items"
:content="{
align: 'start'
}"
:ui="{
content: 'w-48'
}"
>
<PButton label="Open" icon="i-lucide-menu" color="neutral" variant="outline" />
</PDropdownMenu>
</template>Arrow
Use the arrow prop to display an arrow on the DropdownMenu.
<script setup lang="ts">
import type { DropdownMenuItem } from 'pohon-ui'
const items = ref<DropdownMenuItem[]>([
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-cog'
}
])
</script>
<template>
<PDropdownMenu
arrow
:items="items"
:ui="{
content: 'w-48'
}"
>
<PButton label="Open" icon="i-lucide-menu" color="neutral" variant="outline" />
</PDropdownMenu>
</template><script setup lang="ts">
import { ref } from 'vue'
import type { DropdownMenuItem } from 'pohon-ui'
const items = ref<DropdownMenuItem[]>([
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-cog'
}
])
</script>
<template>
<PDropdownMenu
arrow
:items="items"
:ui="{
content: 'w-48'
}"
>
<PButton label="Open" icon="i-lucide-menu" color="neutral" variant="outline" />
</PDropdownMenu>
</template>Size
Use the size prop to control the size of the DropdownMenu.
<script setup lang="ts">
import type { DropdownMenuItem } from 'pohon-ui'
const items = ref<DropdownMenuItem[]>([
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-cog'
}
])
</script>
<template>
<PDropdownMenu
size="xl"
:items="items"
:content="{
align: 'start'
}"
:ui="{
content: 'w-48'
}"
>
<PButton size="xl" label="Open" icon="i-lucide-menu" color="neutral" variant="outline" />
</PDropdownMenu>
</template><script setup lang="ts">
import { ref } from 'vue'
import type { DropdownMenuItem } from 'pohon-ui'
const items = ref<DropdownMenuItem[]>([
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-cog'
}
])
</script>
<template>
<PDropdownMenu
size="xl"
:items="items"
:content="{
align: 'start'
}"
:ui="{
content: 'w-48'
}"
>
<PButton size="xl" label="Open" icon="i-lucide-menu" color="neutral" variant="outline" />
</PDropdownMenu>
</template>size prop will not be proxied to the Button, you need to set it yourself.Modal
Use the modal prop to control whether the DropdownMenu blocks interaction with outside content. Defaults to true.
<script setup lang="ts">
import type { DropdownMenuItem } from 'pohon-ui'
const items = ref<DropdownMenuItem[]>([
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-cog'
}
])
</script>
<template>
<PDropdownMenu
:items="items"
:ui="{
content: 'w-48'
}"
>
<PButton label="Open" icon="i-lucide-menu" color="neutral" variant="outline" />
</PDropdownMenu>
</template><script setup lang="ts">
import { ref } from 'vue'
import type { DropdownMenuItem } from 'pohon-ui'
const items = ref<DropdownMenuItem[]>([
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-cog'
}
])
</script>
<template>
<PDropdownMenu
:items="items"
:ui="{
content: 'w-48'
}"
>
<PButton label="Open" icon="i-lucide-menu" color="neutral" variant="outline" />
</PDropdownMenu>
</template>Disabled
Use the disabled prop to disable the DropdownMenu.
<script setup lang="ts">
import type { DropdownMenuItem } from 'pohon-ui'
const items = ref<DropdownMenuItem[]>([
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-cog'
}
])
</script>
<template>
<PDropdownMenu
disabled
:items="items"
:ui="{
content: 'w-48'
}"
>
<PButton label="Open" icon="i-lucide-menu" color="neutral" variant="outline" />
</PDropdownMenu>
</template><script setup lang="ts">
import { ref } from 'vue'
import type { DropdownMenuItem } from 'pohon-ui'
const items = ref<DropdownMenuItem[]>([
{
label: 'Profile',
icon: 'i-lucide-user'
},
{
label: 'Billing',
icon: 'i-lucide-credit-card'
},
{
label: 'Settings',
icon: 'i-lucide-cog'
}
])
</script>
<template>
<PDropdownMenu
disabled
:items="items"
:ui="{
content: 'w-48'
}"
>
<PButton label="Open" icon="i-lucide-menu" color="neutral" variant="outline" />
</PDropdownMenu>
</template>Examples
With checkbox items
You can use the type property with checkbox and use the checked / onUpdateChecked properties to control the checked state of the item.
<script setup lang="ts">
import type { DropdownMenuItem } from 'pohon-ui';
const showBookmarks = ref(true);
const showHistory = ref(false);
const showDownloads = ref(false);
const items = computed(() => [{
label: 'Interface',
icon: 'i-lucide-app-window',
type: 'label' as const,
}, {
type: 'separator' as const,
}, {
label: 'Show Bookmarks',
icon: 'i-lucide-bookmark',
type: 'checkbox' as const,
checked: showBookmarks.value,
onUpdateChecked(checked: boolean) {
showBookmarks.value = checked;
},
onSelect(e: Event) {
e.preventDefault();
},
}, {
label: 'Show History',
icon: 'i-lucide-clock',
type: 'checkbox' as const,
checked: showHistory.value,
onUpdateChecked(checked: boolean) {
showHistory.value = checked;
},
}, {
label: 'Show Downloads',
icon: 'i-lucide-download',
type: 'checkbox' as const,
checked: showDownloads.value,
onUpdateChecked(checked: boolean) {
showDownloads.value = checked;
},
}] satisfies Array<DropdownMenuItem>);
</script>
<template>
<PDropdownMenu
:items="items"
:content="{ align: 'start' }"
:ui="{ content: 'w-48' }"
>
<PButton
label="Open"
color="neutral"
variant="outline"
icon="i-lucide-menu"
/>
</PDropdownMenu>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { DropdownMenuItem } from 'pohon-ui';
const showBookmarks = ref(true);
const showHistory = ref(false);
const showDownloads = ref(false);
const items = computed(() => [{
label: 'Interface',
icon: 'i-lucide-app-window',
type: 'label' as const,
}, {
type: 'separator' as const,
}, {
label: 'Show Bookmarks',
icon: 'i-lucide-bookmark',
type: 'checkbox' as const,
checked: showBookmarks.value,
onUpdateChecked(checked: boolean) {
showBookmarks.value = checked;
},
onSelect(e: Event) {
e.preventDefault();
},
}, {
label: 'Show History',
icon: 'i-lucide-clock',
type: 'checkbox' as const,
checked: showHistory.value,
onUpdateChecked(checked: boolean) {
showHistory.value = checked;
},
}, {
label: 'Show Downloads',
icon: 'i-lucide-download',
type: 'checkbox' as const,
checked: showDownloads.value,
onUpdateChecked(checked: boolean) {
showDownloads.value = checked;
},
}] satisfies Array<DropdownMenuItem>);
</script>
<template>
<PDropdownMenu
:items="items"
:content="{ align: 'start' }"
:ui="{ content: 'w-48' }"
>
<PButton
label="Open"
color="neutral"
variant="outline"
icon="i-lucide-menu"
/>
</PDropdownMenu>
</template>
checked state of items, it's recommended to wrap your items array inside a computed.With color items
You can use the color property to highlight certain items with a color.
<script setup lang="ts">
import type { DropdownMenuItem } from 'pohon-ui';
const items: Array<Array<DropdownMenuItem>> = [
[
{
label: 'View',
icon: 'i-lucide-eye',
},
{
label: 'Copy',
icon: 'i-lucide-copy',
},
{
label: 'Edit',
icon: 'i-lucide-pencil',
},
],
[
{
label: 'Delete',
color: 'error',
icon: 'i-lucide-trash',
},
],
];
</script>
<template>
<PDropdownMenu
:items="items"
:ui="{ content: 'w-48' }"
>
<PButton
label="Open"
color="neutral"
variant="outline"
icon="i-lucide-menu"
/>
</PDropdownMenu>
</template>
With filter items
You can use the filter property on items with children to display a filter input inside the sub-menu.
<script setup lang="ts">
import type { DropdownMenuItem } from 'pohon-ui';
const items: Array<Array<DropdownMenuItem>> = [[{
label: 'Profile',
icon: 'i-lucide-user',
}, {
label: 'Billing',
icon: 'i-lucide-credit-card',
}, {
label: 'Settings',
icon: 'i-lucide-cog',
}], [{
label: 'Team',
icon: 'i-lucide-users',
filter: {
placeholder: 'Search members...',
},
children: [
{
label: 'benjamincanac',
avatar: { src: 'https://github.com/benjamincanac.png', loading: 'lazy' as const },
},
{
label: 'HugoRCD',
avatar: { src: 'https://github.com/HugoRCD.png', loading: 'lazy' as const },
},
{
label: 'atinux',
avatar: { src: 'https://github.com/atinux.png', loading: 'lazy' as const },
},
{
label: 'romhml',
avatar: { src: 'https://github.com/romhml.png', loading: 'lazy' as const },
},
{
label: 'sandros94',
avatar: { src: 'https://github.com/sandros94.png', loading: 'lazy' as const },
},
{
label: 'J-Michalek',
avatar: { src: 'https://github.com/J-Michalek.png', loading: 'lazy' as const },
},
{
label: 'hywax',
avatar: { src: 'https://github.com/hywax.png', loading: 'lazy' as const },
},
],
}, {
label: 'Invite users',
icon: 'i-lucide-user-plus',
children: [{
label: 'Invite by email',
icon: 'i-lucide-mail',
}, {
label: 'Invite by link',
icon: 'i-lucide-link',
}, {
label: 'Invite by SMS',
icon: 'i-lucide-message-square',
}, {
label: 'Invite by WhatsApp',
icon: 'i-simple-icons-whatsapp',
}],
}, {
label: 'New team',
icon: 'i-lucide-plus',
}]];
</script>
<template>
<PDropdownMenu
:items="items"
:content="{ align: 'start' }"
:ui="{ content: 'w-48' }"
>
<PButton
label="Open"
color="neutral"
variant="outline"
icon="i-lucide-menu"
/>
</PDropdownMenu>
</template>
Control open state
You can control the open state by using the default-open prop or the v-model:open directive.
<script setup lang="ts">
import type { DropdownMenuItem } from 'pohon-ui';
const open = ref(false);
defineShortcuts({
o: () => open.value = !open.value,
});
const items: Array<DropdownMenuItem> = [
{
label: 'Profile',
icon: 'i-lucide-user',
},
{
label: 'Billing',
icon: 'i-lucide-credit-card',
},
{
label: 'Settings',
icon: 'i-lucide-cog',
},
];
</script>
<template>
<PDropdownMenu
v-model:open="open"
:items="items"
:ui="{ content: 'w-48' }"
>
<PButton
label="Open"
color="neutral"
variant="outline"
icon="i-lucide-menu"
/>
</PDropdownMenu>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { DropdownMenuItem } from 'pohon-ui';
const open = ref(false);
defineShortcuts({
o: () => open.value = !open.value,
});
const items: Array<DropdownMenuItem> = [
{
label: 'Profile',
icon: 'i-lucide-user',
},
{
label: 'Billing',
icon: 'i-lucide-credit-card',
},
{
label: 'Settings',
icon: 'i-lucide-cog',
},
];
</script>
<template>
<PDropdownMenu
v-model:open="open"
:items="items"
:ui="{ content: 'w-48' }"
>
<PButton
label="Open"
color="neutral"
variant="outline"
icon="i-lucide-menu"
/>
</PDropdownMenu>
</template>
defineShortcuts, you can toggle the DropdownMenu by pressing O.With custom slot
Use the slot property to customize a specific item.
You will have access to the following slots:
#{{ item.slot }}#{{ item.slot }}-leading#{{ item.slot }}-label#{{ item.slot }}-trailing
<script setup lang="ts">
import type { DropdownMenuItem } from 'pohon-ui';
const items = [
{
label: 'Profile',
icon: 'i-lucide-user',
slot: 'profile' as const,
},
{
label: 'Billing',
icon: 'i-lucide-credit-card',
},
{
label: 'Settings',
icon: 'i-lucide-cog',
},
] satisfies Array<DropdownMenuItem>;
</script>
<template>
<PDropdownMenu
:items="items"
:ui="{ content: 'w-48' }"
>
<PButton
label="Open"
color="neutral"
variant="outline"
icon="i-lucide-menu"
/>
<template #profile-trailing>
<PIcon
name="i-lucide-badge-check"
class="color-primary shrink-0 size-5"
/>
</template>
</PDropdownMenu>
</template>
#item, #item-leading, #item-label and #item-trailing slots to customize all items.With switch in items
You can use the slot property with a #{{ slot }}-trailing slot to render a Switch inside an item.
<script setup lang="ts">
import type { DropdownMenuItem } from 'pohon-ui';
const showBookmarks = ref(true);
const showHistory = ref(false);
const showDownloads = ref(false);
const items = computed(() => [{
label: 'Show Bookmarks',
icon: 'i-lucide-bookmark',
slot: 'switch' as const,
checked: showBookmarks.value,
onSelect(e: Event) {
e.preventDefault();
showBookmarks.value = !showBookmarks.value;
},
}, {
label: 'Show History',
icon: 'i-lucide-clock',
slot: 'switch' as const,
checked: showHistory.value,
onSelect(e: Event) {
e.preventDefault();
showHistory.value = !showHistory.value;
},
}, {
label: 'Show Downloads',
icon: 'i-lucide-download',
slot: 'switch' as const,
checked: showDownloads.value,
onSelect(e: Event) {
e.preventDefault();
showDownloads.value = !showDownloads.value;
},
}] satisfies Array<DropdownMenuItem>);
</script>
<template>
<PDropdownMenu
:items="items"
:content="{ align: 'start' }"
:ui="{ content: 'w-48' }"
>
<PButton
label="Open"
color="neutral"
variant="outline"
icon="i-lucide-menu"
/>
<template #switch-trailing="{ item }">
<PSwitch
:model-value="item.checked"
tabindex="-1"
/>
</template>
</PDropdownMenu>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { DropdownMenuItem } from 'pohon-ui';
const showBookmarks = ref(true);
const showHistory = ref(false);
const showDownloads = ref(false);
const items = computed(() => [{
label: 'Show Bookmarks',
icon: 'i-lucide-bookmark',
slot: 'switch' as const,
checked: showBookmarks.value,
onSelect(e: Event) {
e.preventDefault();
showBookmarks.value = !showBookmarks.value;
},
}, {
label: 'Show History',
icon: 'i-lucide-clock',
slot: 'switch' as const,
checked: showHistory.value,
onSelect(e: Event) {
e.preventDefault();
showHistory.value = !showHistory.value;
},
}, {
label: 'Show Downloads',
icon: 'i-lucide-download',
slot: 'switch' as const,
checked: showDownloads.value,
onSelect(e: Event) {
e.preventDefault();
showDownloads.value = !showDownloads.value;
},
}] satisfies Array<DropdownMenuItem>);
</script>
<template>
<PDropdownMenu
:items="items"
:content="{ align: 'start' }"
:ui="{ content: 'w-48' }"
>
<PButton
label="Open"
color="neutral"
variant="outline"
icon="i-lucide-menu"
/>
<template #switch-trailing="{ item }">
<PSwitch
:model-value="item.checked"
tabindex="-1"
/>
</template>
</PDropdownMenu>
</template>
With ignore filter
When using the filter prop or the filter field on items with children, you can set the ignore-filter prop to true to disable the internal search and use your own search logic.
<script setup lang="ts">
import type { DropdownMenuItem } from 'pohon-ui';
import { refDebounced } from '@vueuse/core';
const searchTerm = ref('');
const searchTermDebounced = refDebounced(searchTerm, 200);
const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
key: 'dropdown-menu-users-search',
query: { q: searchTermDebounced },
transform: (data: Array<{ id: number; name: string }>) => {
return data?.map((user) => ({
label: user.name,
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const },
})) as Array<DropdownMenuItem>;
},
immediate: false,
});
function onOpen() {
if (!users.value?.length) {
execute();
}
}
</script>
<template>
<PDropdownMenu
v-model:search-term="searchTerm"
:items="users || []"
:filter="{
icon: 'i-lucide-search',
loading: status === 'pending',
}"
ignore-filter
:content="{ align: 'start' }"
:ui="{ content: 'w-48' }"
@update:open="onOpen"
>
<PButton
label="Open"
color="neutral"
variant="outline"
icon="i-lucide-menu"
/>
</PDropdownMenu>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { DropdownMenuItem } from 'pohon-ui';
import { refDebounced } from '@vueuse/core';
const searchTerm = ref('');
const searchTermDebounced = refDebounced(searchTerm, 200);
const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
key: 'dropdown-menu-users-search',
query: { q: searchTermDebounced },
transform: (data: Array<{ id: number; name: string }>) => {
return data?.map((user) => ({
label: user.name,
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, loading: 'lazy' as const },
})) as Array<DropdownMenuItem>;
},
immediate: false,
});
function onOpen() {
if (!users.value?.length) {
execute();
}
}
</script>
<template>
<PDropdownMenu
v-model:search-term="searchTerm"
:items="users || []"
:filter="{
icon: 'i-lucide-search',
loading: status === 'pending',
}"
ignore-filter
:content="{ align: 'start' }"
:ui="{ content: 'w-48' }"
@update:open="onOpen"
>
<PButton
label="Open"
color="neutral"
variant="outline"
icon="i-lucide-menu"
/>
</PDropdownMenu>
</template>
refDebounced to debounce the API calls. The fetch is deferred with immediate: false so no request is made until the menu opens.With filter fields
When using the filter prop or the filter field on items with children, you can set the filter-fields prop with an array of fields to filter on. Defaults to [labelKey].
<script setup lang="ts">
import type { DropdownMenuItem } from 'pohon-ui';
const items: Array<DropdownMenuItem> = [{
label: 'Leanne Graham',
description: 'Sincere@april.biz',
}, {
label: 'Ervin Howell',
description: 'Shanna@melissa.tv',
}, {
label: 'Clementine Bauch',
description: 'Nathan@yesenia.net',
}, {
label: 'Patricia Lebsack',
description: 'Julianne.OConner@kory.org',
}, {
label: 'Chelsey Dietrich',
description: 'Lucio_Hettinger@annie.me',
}];
</script>
<template>
<PDropdownMenu
:items="items"
filter
:filter-fields="['label', 'description']"
:content="{ align: 'start' }"
:ui="{ content: 'w-64' }"
>
<PButton
label="Open"
color="neutral"
variant="outline"
icon="i-lucide-menu"
/>
</PDropdownMenu>
</template>
With trigger content width
You can expand the content to the full width of its button by adding the w-(--akar-dropdown-menu-trigger-width) class on the ui.content slot.
<script setup lang="ts">
import type { DropdownMenuItem } from 'pohon-ui';
const items: Array<Array<DropdownMenuItem>> = [
[
{
label: 'View',
icon: 'i-lucide-eye',
},
{
label: 'Copy',
icon: 'i-lucide-copy',
},
{
label: 'Edit',
icon: 'i-lucide-pencil',
},
],
[
{
label: 'Delete',
color: 'error',
icon: 'i-lucide-trash',
},
],
];
</script>
<template>
<PDropdownMenu
:items="items"
:ui="{ content: 'w-(--akar-dropdown-menu-trigger-width)' }"
>
<PButton
label="Open"
class="w-46"
color="neutral"
variant="outline"
block
trailing-icon="i-lucide-chevron-down"
/>
</PDropdownMenu>
</template>
app.config.ts:export default defineAppConfig({
ui: {
dropdownMenu: {
slots: {
content: 'w-(--akar-dropdown-menu-trigger-width)'
}
}
}
})
Extract shortcuts
Use the extractShortcuts utility to automatically define shortcuts from menu items with a kbds property. It recursively extracts shortcuts and returns an object compatible with defineShortcuts.
<script setup lang="ts">
import type { DropdownMenuItem } from 'pohon-ui';
const items: Array<DropdownMenuItem> = [{
label: 'Invite users',
icon: 'i-lucide-user-plus',
children: [{
label: 'Invite by email',
icon: 'i-lucide-send-horizontal',
kbds: ['meta', 'e'],
onSelect() {
console.log('Invite by email clicked');
}
}, {
label: 'Invite by link',
icon: 'i-lucide-link',
kbds: ['meta', 'i'],
onSelect() {
console.log('Invite by link clicked');
}
}]
}, {
label: 'New team',
icon: 'i-lucide-plus',
kbds: ['meta', 'n'],
onSelect() {
console.log('New team clicked');
}
}];
defineShortcuts(extractShortcuts(items));
</script>
select function of the corresponding item.API
Props
| Prop | Default | Type |
|---|
Slots
| Slot | Type |
|---|
Emits
| Event | Type |
|---|
Theme
export default defineAppConfig({
ui: {
dropdownMenu: {
slots: {
content: 'bg-background ring-border rounded-md flex flex-col max-h-$akar-dropdown-menu-content-available-height min-w-32 ring shadow-lg origin-$akar-dropdown-menu-content-transform-origin overflow-hidden data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
input: 'border-b border-border',
empty: 'text-center color-text-muted',
viewport: 'relative divide-y divide-border scroll-py-1 overflow-y-auto flex-1',
arrow: 'fill-background stroke-border',
group: 'p-1 isolate',
label: 'w-full flex items-center font-600 color-text-highlighted',
separator: '-mx-1 my-1 h-px bg-border',
item: 'group outline-none flex w-full select-none items-start relative before:rounded-md before:content-empty before:inset-px before:absolute before:-z-1 data-[disabled]:opacity-75 data-[disabled]:cursor-not-allowed',
itemLeadingIcon: 'shrink-0',
itemLeadingAvatar: 'shrink-0',
itemLeadingAvatarSize: '',
itemTrailing: 'ms-auto inline-flex gap-1.5 items-center',
itemTrailingIcon: 'shrink-0',
itemTrailingKbds: 'hidden lg:inline-flex items-center shrink-0',
itemTrailingKbdsSize: '',
itemWrapper: 'flex-1 flex flex-col text-start min-w-0',
itemLabel: 'truncate',
itemDescription: 'truncate color-text-muted',
itemLabelExternalIcon: 'inline-block size-3 align-top color-text-dimmed'
},
variants: {
color: {
primary: '',
secondary: '',
success: '',
info: '',
warning: '',
error: '',
neutral: ''
},
active: {
true: {
item: 'color-text-highlighted before:bg-background-elevated',
itemLeadingIcon: 'color-text'
},
false: {
item: [
'color-text data-[highlighted]:color-text-highlighted data-[state=open]:color-text-highlighted data-[highlighted]:before:bg-background-elevated/50 data-[state=open]:before:bg-background-elevated/50',
'transition-colors before:transition-colors'
],
itemLeadingIcon: [
'color-text-dimmed group-data-[highlighted]:color-text group-data-[state=open]:color-text',
'transition-colors before:transition-colors'
]
}
},
loading: {
true: {
itemLeadingIcon: 'animate-spin'
}
},
size: {
xs: {
label: 'p-1 text-xs gap-1',
item: 'p-1 text-xs gap-1',
empty: 'p-2 text-xs',
itemLeadingIcon: 'size-4',
itemLeadingAvatarSize: '3xs',
itemTrailingIcon: 'size-4',
itemTrailingKbds: 'gap-0.5',
itemTrailingKbdsSize: 'sm'
},
sm: {
label: 'p-1.5 text-xs gap-1.5',
item: 'p-1.5 text-xs gap-1.5',
empty: 'p-2.5 text-xs',
itemLeadingIcon: 'size-4',
itemLeadingAvatarSize: '3xs',
itemTrailingIcon: 'size-4',
itemTrailingKbds: 'gap-0.5',
itemTrailingKbdsSize: 'sm'
},
md: {
label: 'p-1.5 text-sm gap-1.5',
item: 'p-1.5 text-sm gap-1.5',
empty: 'p-2.5 text-sm',
itemLeadingIcon: 'size-5',
itemLeadingAvatarSize: '2xs',
itemTrailingIcon: 'size-5',
itemTrailingKbds: 'gap-0.5',
itemTrailingKbdsSize: 'md'
},
lg: {
label: 'p-2 text-sm gap-2',
item: 'p-2 text-sm gap-2',
empty: 'p-3 text-sm',
itemLeadingIcon: 'size-5',
itemLeadingAvatarSize: '2xs',
itemTrailingIcon: 'size-5',
itemTrailingKbds: 'gap-1',
itemTrailingKbdsSize: 'md'
},
xl: {
label: 'p-2 text-base gap-2',
item: 'p-2 text-base gap-2',
empty: 'p-3 text-base',
itemLeadingIcon: 'size-6',
itemLeadingAvatarSize: 'xs',
itemTrailingIcon: 'size-6',
itemTrailingKbds: 'gap-1',
itemTrailingKbdsSize: 'lg'
}
}
},
compoundVariants: [
{
color: 'primary',
active: false,
class: {
item: 'color-primary data-[highlighted]:color-primary data-[highlighted]:before:bg-primary/10 data-[state=open]:before:bg-primary/10',
itemLeadingIcon: 'color-primary/75 group-data-[highlighted]:color-primary group-data-[state=open]:color-primary'
}
},
{
color: 'primary',
active: true,
class: {
item: 'color-primary before:bg-primary/10',
itemLeadingIcon: 'color-primary'
}
}
],
defaultVariants: {
size: 'md'
}
}
}
})import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from 'pohon-ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
dropdownMenu: {
slots: {
content: 'bg-background ring-border rounded-md flex flex-col max-h-$akar-dropdown-menu-content-available-height min-w-32 ring shadow-lg origin-$akar-dropdown-menu-content-transform-origin overflow-hidden data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
input: 'border-b border-border',
empty: 'text-center color-text-muted',
viewport: 'relative divide-y divide-border scroll-py-1 overflow-y-auto flex-1',
arrow: 'fill-background stroke-border',
group: 'p-1 isolate',
label: 'w-full flex items-center font-600 color-text-highlighted',
separator: '-mx-1 my-1 h-px bg-border',
item: 'group outline-none flex w-full select-none items-start relative before:rounded-md before:content-empty before:inset-px before:absolute before:-z-1 data-[disabled]:opacity-75 data-[disabled]:cursor-not-allowed',
itemLeadingIcon: 'shrink-0',
itemLeadingAvatar: 'shrink-0',
itemLeadingAvatarSize: '',
itemTrailing: 'ms-auto inline-flex gap-1.5 items-center',
itemTrailingIcon: 'shrink-0',
itemTrailingKbds: 'hidden lg:inline-flex items-center shrink-0',
itemTrailingKbdsSize: '',
itemWrapper: 'flex-1 flex flex-col text-start min-w-0',
itemLabel: 'truncate',
itemDescription: 'truncate color-text-muted',
itemLabelExternalIcon: 'inline-block size-3 align-top color-text-dimmed'
},
variants: {
color: {
primary: '',
secondary: '',
success: '',
info: '',
warning: '',
error: '',
neutral: ''
},
active: {
true: {
item: 'color-text-highlighted before:bg-background-elevated',
itemLeadingIcon: 'color-text'
},
false: {
item: [
'color-text data-[highlighted]:color-text-highlighted data-[state=open]:color-text-highlighted data-[highlighted]:before:bg-background-elevated/50 data-[state=open]:before:bg-background-elevated/50',
'transition-colors before:transition-colors'
],
itemLeadingIcon: [
'color-text-dimmed group-data-[highlighted]:color-text group-data-[state=open]:color-text',
'transition-colors before:transition-colors'
]
}
},
loading: {
true: {
itemLeadingIcon: 'animate-spin'
}
},
size: {
xs: {
label: 'p-1 text-xs gap-1',
item: 'p-1 text-xs gap-1',
empty: 'p-2 text-xs',
itemLeadingIcon: 'size-4',
itemLeadingAvatarSize: '3xs',
itemTrailingIcon: 'size-4',
itemTrailingKbds: 'gap-0.5',
itemTrailingKbdsSize: 'sm'
},
sm: {
label: 'p-1.5 text-xs gap-1.5',
item: 'p-1.5 text-xs gap-1.5',
empty: 'p-2.5 text-xs',
itemLeadingIcon: 'size-4',
itemLeadingAvatarSize: '3xs',
itemTrailingIcon: 'size-4',
itemTrailingKbds: 'gap-0.5',
itemTrailingKbdsSize: 'sm'
},
md: {
label: 'p-1.5 text-sm gap-1.5',
item: 'p-1.5 text-sm gap-1.5',
empty: 'p-2.5 text-sm',
itemLeadingIcon: 'size-5',
itemLeadingAvatarSize: '2xs',
itemTrailingIcon: 'size-5',
itemTrailingKbds: 'gap-0.5',
itemTrailingKbdsSize: 'md'
},
lg: {
label: 'p-2 text-sm gap-2',
item: 'p-2 text-sm gap-2',
empty: 'p-3 text-sm',
itemLeadingIcon: 'size-5',
itemLeadingAvatarSize: '2xs',
itemTrailingIcon: 'size-5',
itemTrailingKbds: 'gap-1',
itemTrailingKbdsSize: 'md'
},
xl: {
label: 'p-2 text-base gap-2',
item: 'p-2 text-base gap-2',
empty: 'p-3 text-base',
itemLeadingIcon: 'size-6',
itemLeadingAvatarSize: 'xs',
itemTrailingIcon: 'size-6',
itemTrailingKbds: 'gap-1',
itemTrailingKbdsSize: 'lg'
}
}
},
compoundVariants: [
{
color: 'primary',
active: false,
class: {
item: 'color-primary data-[highlighted]:color-primary data-[highlighted]:before:bg-primary/10 data-[state=open]:before:bg-primary/10',
itemLeadingIcon: 'color-primary/75 group-data-[highlighted]:color-primary group-data-[state=open]:color-primary'
}
},
{
color: 'primary',
active: true,
class: {
item: 'color-primary before:bg-primary/10',
itemLeadingIcon: 'color-primary'
}
}
],
defaultVariants: {
size: 'md'
}
}
}
})
]
})