ContextMenu

A menu to display actions when right-clicking on an element.

Usage

Use anything you like in the default slot of the ContextMenu, and right-click on it to display the menu.

Right click here
<script setup lang="ts">
import type { ContextMenuItem } from 'pohon-ui'

const items = ref<ContextMenuItem[][]>([
  [
    {
      label: 'Appearance',
      children: [
        {
          label: 'System',
          icon: 'i-lucide-monitor'
        },
        {
          label: 'Light',
          icon: 'i-lucide-sun'
        },
        {
          label: 'Dark',
          icon: 'i-lucide-moon'
        }
      ]
    }
  ],
  [
    {
      label: 'Show Sidebar',
      kbds: ['meta', 's']
    },
    {
      label: 'Show Toolbar',
      kbds: ['shift', 'meta', 'd']
    },
    {
      label: 'Collapse Pinned Tabs',
      disabled: true
    }
  ],
  [
    {
      label: 'Refresh the Page'
    },
    {
      label: 'Clear Cookies and Refresh'
    },
    {
      label: 'Clear Cache and Refresh'
    },
    {
      type: 'separator'
    },
    {
      label: 'Developer',
      children: [
        [
          {
            label: 'View Source',
            kbds: ['meta', 'shift', 'u']
          },
          {
            label: 'Developer Tools',
            kbds: ['option', 'meta', 'i']
          },
          {
            label: 'Inspect Elements',
            kbds: ['option', 'meta', 'c']
          }
        ],
        [
          {
            label: 'JavaScript Console',
            kbds: ['option', 'meta', 'j']
          }
        ]
      ]
    }
  ]
])
</script>

<template>
  <PContextMenu :items="items">
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-border-accented text-sm aspect-video w-72"
    >
      Right click here
    </div>
  </PContextMenu>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ContextMenuItem } from 'pohon-ui'

const items = ref<ContextMenuItem[][]>([
  [
    {
      label: 'Appearance',
      children: [
        {
          label: 'System',
          icon: 'i-lucide-monitor'
        },
        {
          label: 'Light',
          icon: 'i-lucide-sun'
        },
        {
          label: 'Dark',
          icon: 'i-lucide-moon'
        }
      ]
    }
  ],
  [
    {
      label: 'Show Sidebar',
      kbds: ['meta', 's']
    },
    {
      label: 'Show Toolbar',
      kbds: ['shift', 'meta', 'd']
    },
    {
      label: 'Collapse Pinned Tabs',
      disabled: true
    }
  ],
  [
    {
      label: 'Refresh the Page'
    },
    {
      label: 'Clear Cookies and Refresh'
    },
    {
      label: 'Clear Cache and Refresh'
    },
    {
      type: 'separator'
    },
    {
      label: 'Developer',
      children: [
        [
          {
            label: 'View Source',
            kbds: ['meta', 'shift', 'u']
          },
          {
            label: 'Developer Tools',
            kbds: ['option', 'meta', 'i']
          },
          {
            label: 'Inspect Elements',
            kbds: ['option', 'meta', 'c']
          }
        ],
        [
          {
            label: 'JavaScript Console',
            kbds: ['option', 'meta', 'j']
          }
        ]
      ]
    }
  ]
])
</script>

<template>
  <PContextMenu :items="items">
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-border-accented text-sm aspect-video w-72"
    >
      Right click here
    </div>
  </PContextMenu>
</template>

Items

Use the items prop as an array of objects with the following properties:

You can pass any property from the Link component such as to, target, etc.

Right click here
<script setup lang="ts">
import type { ContextMenuItem } from 'pohon-ui'

const items = ref<ContextMenuItem[][]>([
  [
    {
      label: 'Appearance',
      children: [
        {
          label: 'System',
          icon: 'i-lucide-monitor'
        },
        {
          label: 'Light',
          icon: 'i-lucide-sun'
        },
        {
          label: 'Dark',
          icon: 'i-lucide-moon'
        }
      ]
    }
  ],
  [
    {
      label: 'Show Sidebar',
      kbds: ['meta', 's']
    },
    {
      label: 'Show Toolbar',
      kbds: ['shift', 'meta', 'd']
    },
    {
      label: 'Collapse Pinned Tabs',
      disabled: true
    }
  ],
  [
    {
      label: 'Refresh the Page'
    },
    {
      label: 'Clear Cookies and Refresh'
    },
    {
      label: 'Clear Cache and Refresh'
    },
    {
      type: 'separator'
    },
    {
      label: 'Developer',
      children: [
        [
          {
            label: 'View Source',
            kbds: ['meta', 'shift', 'u']
          },
          {
            label: 'Developer Tools',
            kbds: ['option', 'meta', 'i']
          },
          {
            label: 'Inspect Elements',
            kbds: ['option', 'meta', 'c']
          }
        ],
        [
          {
            label: 'JavaScript Console',
            kbds: ['option', 'meta', 'j']
          }
        ]
      ]
    }
  ]
])
</script>

<template>
  <PContextMenu
    :items="items"
    :ui="{
      content: 'w-48'
    }"
  >
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-border-accented text-sm aspect-video w-72"
    >
      Right click here
    </div>
  </PContextMenu>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ContextMenuItem } from 'pohon-ui'

const items = ref<ContextMenuItem[][]>([
  [
    {
      label: 'Appearance',
      children: [
        {
          label: 'System',
          icon: 'i-lucide-monitor'
        },
        {
          label: 'Light',
          icon: 'i-lucide-sun'
        },
        {
          label: 'Dark',
          icon: 'i-lucide-moon'
        }
      ]
    }
  ],
  [
    {
      label: 'Show Sidebar',
      kbds: ['meta', 's']
    },
    {
      label: 'Show Toolbar',
      kbds: ['shift', 'meta', 'd']
    },
    {
      label: 'Collapse Pinned Tabs',
      disabled: true
    }
  ],
  [
    {
      label: 'Refresh the Page'
    },
    {
      label: 'Clear Cookies and Refresh'
    },
    {
      label: 'Clear Cache and Refresh'
    },
    {
      type: 'separator'
    },
    {
      label: 'Developer',
      children: [
        [
          {
            label: 'View Source',
            kbds: ['meta', 'shift', 'u']
          },
          {
            label: 'Developer Tools',
            kbds: ['option', 'meta', 'i']
          },
          {
            label: 'Inspect Elements',
            kbds: ['option', 'meta', 'c']
          }
        ],
        [
          {
            label: 'JavaScript Console',
            kbds: ['option', 'meta', 'j']
          }
        ]
      ]
    }
  ]
])
</script>

<template>
  <PContextMenu
    :items="items"
    :ui="{
      content: 'w-48'
    }"
  >
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-border-accented text-sm aspect-video w-72"
    >
      Right click here
    </div>
  </PContextMenu>
</template>
You can also pass an array of arrays to the items prop to create separated groups of items.
Each item can take a 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.

Size

Use the size prop to change the size of the ContextMenu.

Right click here
<script setup lang="ts">
import type { ContextMenuItem } from 'pohon-ui'

const items = ref<ContextMenuItem[]>([
  {
    label: 'System',
    icon: 'i-lucide-monitor'
  },
  {
    label: 'Light',
    icon: 'i-lucide-sun'
  },
  {
    label: 'Dark',
    icon: 'i-lucide-moon'
  }
])
</script>

<template>
  <PContextMenu
    size="xl"
    :items="items"
    :ui="{
      content: 'w-48'
    }"
  >
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-border-accented text-sm aspect-video w-72"
    >
      Right click here
    </div>
  </PContextMenu>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ContextMenuItem } from 'pohon-ui'

const items = ref<ContextMenuItem[]>([
  {
    label: 'System',
    icon: 'i-lucide-monitor'
  },
  {
    label: 'Light',
    icon: 'i-lucide-sun'
  },
  {
    label: 'Dark',
    icon: 'i-lucide-moon'
  }
])
</script>

<template>
  <PContextMenu
    size="xl"
    :items="items"
    :ui="{
      content: 'w-48'
    }"
  >
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-border-accented text-sm aspect-video w-72"
    >
      Right click here
    </div>
  </PContextMenu>
</template>

Use the modal prop to control whether the ContextMenu blocks interaction with outside content. Defaults to true.

Right click here
<script setup lang="ts">
import type { ContextMenuItem } from 'pohon-ui'

const items = ref<ContextMenuItem[]>([
  {
    label: 'System',
    icon: 'i-lucide-monitor'
  },
  {
    label: 'Light',
    icon: 'i-lucide-sun'
  },
  {
    label: 'Dark',
    icon: 'i-lucide-moon'
  }
])
</script>

<template>
  <PContextMenu
    :items="items"
    :ui="{
      content: 'w-48'
    }"
  >
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-border-accented text-sm aspect-video w-72"
    >
      Right click here
    </div>
  </PContextMenu>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ContextMenuItem } from 'pohon-ui'

const items = ref<ContextMenuItem[]>([
  {
    label: 'System',
    icon: 'i-lucide-monitor'
  },
  {
    label: 'Light',
    icon: 'i-lucide-sun'
  },
  {
    label: 'Dark',
    icon: 'i-lucide-moon'
  }
])
</script>

<template>
  <PContextMenu
    :items="items"
    :ui="{
      content: 'w-48'
    }"
  >
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-border-accented text-sm aspect-video w-72"
    >
      Right click here
    </div>
  </PContextMenu>
</template>

Disabled

Use the disabled prop to disable the ContextMenu.

Right click here
<script setup lang="ts">
import type { ContextMenuItem } from 'pohon-ui'

const items = ref<ContextMenuItem[]>([
  {
    label: 'System',
    icon: 'i-lucide-monitor'
  },
  {
    label: 'Light',
    icon: 'i-lucide-sun'
  },
  {
    label: 'Dark',
    icon: 'i-lucide-moon'
  }
])
</script>

<template>
  <PContextMenu
    disabled
    :items="items"
    :ui="{
      content: 'w-48'
    }"
  >
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-border-accented text-sm aspect-video w-72"
    >
      Right click here
    </div>
  </PContextMenu>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ContextMenuItem } from 'pohon-ui'

const items = ref<ContextMenuItem[]>([
  {
    label: 'System',
    icon: 'i-lucide-monitor'
  },
  {
    label: 'Light',
    icon: 'i-lucide-sun'
  },
  {
    label: 'Dark',
    icon: 'i-lucide-moon'
  }
])
</script>

<template>
  <PContextMenu
    disabled
    :items="items"
    :ui="{
      content: 'w-48'
    }"
  >
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-border-accented text-sm aspect-video w-72"
    >
      Right click here
    </div>
  </PContextMenu>
</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.

Right click here
<script setup lang="ts">
import type { ContextMenuItem } from 'pohon-ui';

const showSidebar = ref(true);
const showToolbar = ref(false);

const items = computed<Array<ContextMenuItem>>(() => [{
  label: 'View',
  type: 'label' as const,
}, {
  type: 'separator' as const,
}, {
  label: 'Show Sidebar',
  type: 'checkbox' as const,
  checked: showSidebar.value,
  onUpdateChecked(checked: boolean) {
    showSidebar.value = checked;
  },
  onSelect(e: Event) {
    e.preventDefault();
  },
}, {
  label: 'Show Toolbar',
  type: 'checkbox' as const,
  checked: showToolbar.value,
  onUpdateChecked(checked: boolean) {
    showToolbar.value = checked;
  },
}, {
  label: 'Collapse Pinned Tabs',
  type: 'checkbox' as const,
  disabled: true,
}]);
</script>

<template>
  <PContextMenu
    :items="items"
    :ui="{ content: 'w-48' }"
  >
    <div class="text-sm border border-border-accented rounded-md border-dashed flex w-72 aspect-video items-center justify-center">
      Right click here
    </div>
  </PContextMenu>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { ContextMenuItem } from 'pohon-ui';

const showSidebar = ref(true);
const showToolbar = ref(false);

const items = computed<Array<ContextMenuItem>>(() => [{
  label: 'View',
  type: 'label' as const,
}, {
  type: 'separator' as const,
}, {
  label: 'Show Sidebar',
  type: 'checkbox' as const,
  checked: showSidebar.value,
  onUpdateChecked(checked: boolean) {
    showSidebar.value = checked;
  },
  onSelect(e: Event) {
    e.preventDefault();
  },
}, {
  label: 'Show Toolbar',
  type: 'checkbox' as const,
  checked: showToolbar.value,
  onUpdateChecked(checked: boolean) {
    showToolbar.value = checked;
  },
}, {
  label: 'Collapse Pinned Tabs',
  type: 'checkbox' as const,
  disabled: true,
}]);
</script>

<template>
  <PContextMenu
    :items="items"
    :ui="{ content: 'w-48' }"
  >
    <div class="text-sm border border-border-accented rounded-md border-dashed flex w-72 aspect-video items-center justify-center">
      Right click here
    </div>
  </PContextMenu>
</template>
To ensure reactivity for the 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.

Right click here
<script setup lang="ts">
import type { ContextMenuItem } from 'pohon-ui';

const items: Array<Array<ContextMenuItem>> = [
  [
    {
      label: 'View',
      icon: 'i-lucide-eye',
    },
    {
      label: 'Copy',
      icon: 'i-lucide-copy',
    },
    {
      label: 'Edit',
      icon: 'i-lucide-pencil',
    },
  ],
  [
    {
      label: 'Delete',
      color: 'error' as const,
      icon: 'i-lucide-trash',
    },
  ],
];
</script>

<template>
  <PContextMenu
    :items="items"
    :ui="{ content: 'w-48' }"
  >
    <div class="text-sm border border-border-accented rounded-md border-dashed flex w-72 aspect-video items-center justify-center">
      Right click here
    </div>
  </PContextMenu>
</template>

Control open state

You can control the open state by using the v-model:open directive. When opened programmatically, the menu is anchored to the trigger instead of the cursor.

Right click here
<script setup lang="ts">
import type { ContextMenuItem } from 'pohon-ui';

const open = ref(false);

defineShortcuts({
  o: () => open.value = !open.value,
});

const items: Array<ContextMenuItem> = [
  {
    label: 'Profile',
    icon: 'i-lucide-user',
  },
  {
    label: 'Billing',
    icon: 'i-lucide-credit-card',
  },
  {
    label: 'Settings',
    icon: 'i-lucide-cog',
  },
];
</script>

<template>
  <PContextMenu
    v-model:open="open"
    :items="items"
    :ui="{ content: 'w-48' }"
  >
    <div class="text-sm border border-border-accented rounded-md border-dashed flex w-72 aspect-video items-center justify-center">
      Right click here
    </div>
  </PContextMenu>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ContextMenuItem } from 'pohon-ui';

const open = ref(false);

defineShortcuts({
  o: () => open.value = !open.value,
});

const items: Array<ContextMenuItem> = [
  {
    label: 'Profile',
    icon: 'i-lucide-user',
  },
  {
    label: 'Billing',
    icon: 'i-lucide-credit-card',
  },
  {
    label: 'Settings',
    icon: 'i-lucide-cog',
  },
];
</script>

<template>
  <PContextMenu
    v-model:open="open"
    :items="items"
    :ui="{ content: 'w-48' }"
  >
    <div class="text-sm border border-border-accented rounded-md border-dashed flex w-72 aspect-video items-center justify-center">
      Right click here
    </div>
  </PContextMenu>
</template>
In this example, leveraging defineShortcuts, you can toggle the ContextMenu 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
Right click here
<script setup lang="ts">
import type { ContextMenuItem } from 'pohon-ui';

const loading = ref(true);

const items = [
  {
    label: 'Refresh the Page',
    slot: 'refresh' as const,
  },
  {
    label: 'Clear Cookies and Refresh',
  },
  {
    label: 'Clear Cache and Refresh',
  },
] satisfies Array<ContextMenuItem>;
</script>

<template>
  <PContextMenu
    :items="items"
    :ui="{ content: 'w-48' }"
  >
    <div class="text-sm border border-border-accented rounded-md border-dashed flex w-72 aspect-video items-center justify-center">
      Right click here
    </div>

    <template #refresh-label>
      {{ loading ? 'Refreshing...' : 'Refresh the Page' }}
    </template>

    <template #refresh-trailing>
      <PIcon
        v-if="loading"
        name="i-lucide-loader-circle"
        class="color-primary shrink-0 size-5 animate-spin"
      />
    </template>
  </PContextMenu>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ContextMenuItem } from 'pohon-ui';

const loading = ref(true);

const items = [
  {
    label: 'Refresh the Page',
    slot: 'refresh' as const,
  },
  {
    label: 'Clear Cookies and Refresh',
  },
  {
    label: 'Clear Cache and Refresh',
  },
] satisfies Array<ContextMenuItem>;
</script>

<template>
  <PContextMenu
    :items="items"
    :ui="{ content: 'w-48' }"
  >
    <div class="text-sm border border-border-accented rounded-md border-dashed flex w-72 aspect-video items-center justify-center">
      Right click here
    </div>

    <template #refresh-label>
      {{ loading ? 'Refreshing...' : 'Refresh the Page' }}
    </template>

    <template #refresh-trailing>
      <PIcon
        v-if="loading"
        name="i-lucide-loader-circle"
        class="color-primary shrink-0 size-5 animate-spin"
      />
    </template>
  </PContextMenu>
</template>
You can also use the #item, #item-leading, #item-label and #item-trailing slots to customize all items.

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">
const items = [
  [{
    label: 'Show Sidebar',
    kbds: ['meta', 'S'],
    onSelect() {
      console.log('Show Sidebar clicked');
    }
  }, {
    label: 'Show Toolbar',
    kbds: ['shift', 'meta', 'D'],
    onSelect() {
      console.log('Show Toolbar clicked');
    }
  }, {
    label: 'Collapse Pinned Tabs',
    disabled: true
  }],
  [{
    label: 'Refresh the Page'
  }, {
    label: 'Clear Cookies and Refresh'
  }, {
    label: 'Clear Cache and Refresh'
  }, {
    type: 'separator' as const
  }, {
    label: 'Developer',
    children: [[{
      label: 'View Source',
      kbds: ['option', 'meta', 'U'],
      onSelect() {
        console.log('View Source clicked');
      }
    }, {
      label: 'Developer Tools',
      kbds: ['option', 'meta', 'I'],
      onSelect() {
        console.log('Developer Tools clicked');
      }
    }], [{
      label: 'Inspect Elements',
      kbds: ['option', 'meta', 'C'],
      onSelect() {
        console.log('Inspect Elements clicked');
      }
    }], [{
      label: 'JavaScript Console',
      kbds: ['option', 'meta', 'J'],
      onSelect() {
        console.log('JavaScript Console clicked');
      }
    }]]
  }]
];

defineShortcuts(extractShortcuts(items));
</script>
In this example, CtrlS, ⇧CtrlD, ⌥CtrlU, ⌥CtrlI, ⌥CtrlC and ⌥CtrlJ would trigger the select function of the corresponding item.

API

Props

Prop Default Type

Slots

Slot Type

Emits

Event Type

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    contextMenu: {
      slots: {
        content: 'bg-background ring-border rounded-md flex flex-col max-h-$akar-context-menu-content-available-height min-w-32 ring shadow-lg origin-$akar-context-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',
        viewport: 'relative divide-y divide-border scroll-py-1 overflow-y-auto flex-1',
        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:inset-px before:absolute before:-z-1 before:content-empty 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'
            ]
          }
        },
        loading: {
          true: {
            itemLeadingIcon: 'animate-spin'
          }
        },
        size: {
          xs: {
            label: 'p-1 text-xs gap-1',
            item: 'p-1 text-xs gap-1',
            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',
            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',
            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',
            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',
            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'
      }
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from 'pohon-ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        contextMenu: {
          slots: {
            content: 'bg-background ring-border rounded-md flex flex-col max-h-$akar-context-menu-content-available-height min-w-32 ring shadow-lg origin-$akar-context-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',
            viewport: 'relative divide-y divide-border scroll-py-1 overflow-y-auto flex-1',
            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:inset-px before:absolute before:-z-1 before:content-empty 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'
                ]
              }
            },
            loading: {
              true: {
                itemLeadingIcon: 'animate-spin'
              }
            },
            size: {
              xs: {
                label: 'p-1 text-xs gap-1',
                item: 'p-1 text-xs gap-1',
                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',
                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',
                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',
                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',
                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'
          }
        }
      }
    })
  ]
})
Some colors in compoundVariants are omitted for readability. Check out the source code on GitHub.

Changelog

No recent changes