ContextMenu

A pop-up menu that appears upon right-clicking an element to present relevant actions.

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 '@bitrix24/b24ui-nuxt'

const items = ref<ContextMenuItem[][]>([
  [
    {
      label: 'Appearance',
      children: [
        {
          label: 'System'
        },
        {
          label: 'Light'
        },
        {
          label: 'Dark'
        }
      ]
    }
  ],
  [
    {
      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>
  <B24ContextMenu :items="items">
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-(--ui-color-design-outline-na-stroke) bg-(--ui-color-bg-content-primary) text-(length:--ui-font-size-sm) aspect-video w-72"
    >
      Right click here
    </div>
  </B24ContextMenu>
</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 '@bitrix24/b24ui-nuxt'

const items = ref<ContextMenuItem[][]>([
  [
    {
      label: 'Appearance',
      children: [
        {
          label: 'System'
        },
        {
          label: 'Light'
        },
        {
          label: 'Dark'
        }
      ]
    }
  ],
  [
    {
      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>
  <B24ContextMenu :items="items">
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-(--ui-color-design-outline-na-stroke) text-(length:--ui-font-size-sm) bg-(--ui-color-bg-content-primary) aspect-video w-72"
    >
      Right click here
    </div>
  </B24ContextMenu>
</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.

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 '@bitrix24/b24ui-nuxt'

const items = ref<ContextMenuItem[]>([
  {
    label: 'System'
  },
  {
    label: 'Light'
  },
  {
    label: 'Dark'
  }
])
</script>

<template>
  <B24ContextMenu
    :modal="false"
    :items="items"
    :b24ui="{
      content: 'w-48'
    }"
  >
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-(--ui-color-design-outline-na-stroke) text-(length:--ui-font-size-sm) bg-(--ui-color-bg-content-primary) aspect-video w-72"
    >
      Right click here
    </div>
  </B24ContextMenu>
</template>

Disabled

Use the disabled prop to disable the ContextMenu.

Right click here
<script setup lang="ts">
import type { ContextMenuItem } from '@bitrix24/b24ui-nuxt'

const items = ref<ContextMenuItem[]>([
  {
    label: 'System'
  },
  {
    label: 'Light'
  },
  {
    label: 'Dark'
  }
])
</script>

<template>
  <B24ContextMenu disabled :items="items">
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-(--ui-color-design-outline-na-stroke) text-(length:--ui-font-size-sm) bg-(--ui-color-bg-content-primary) aspect-video w-72"
    >
      Right click here
    </div>
  </B24ContextMenu>
</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 '@bitrix24/b24ui-nuxt'

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

const items = computed<ContextMenuItem[]>(() => [
  {
    label: 'View',
    type: 'label' 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
    }
  },
  {
    type: 'separator' as const
  },
  {
    label: 'Collapse Pinned Tabs',
    type: 'checkbox' as const,
    disabled: true
  }
])
</script>

<template>
  <B24ContextMenu :items="items">
    <div class="flex items-center justify-center rounded-md border border-dashed border-(--ui-color-design-outline-na-stroke) text-(--b24ui-typography-legend-color) text-(length:--ui-font-size-sm) bg-(--ui-color-bg-content-primary) aspect-video w-72">
      Right click here
    </div>
  </B24ContextMenu>
</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 '@bitrix24/b24ui-nuxt'
import OpenedEyeIcon from '@bitrix24/b24icons-vue/main/OpenedEyeIcon'
import TrashBinIcon from '@bitrix24/b24icons-vue/main/TrashBinIcon'
import CopyPlatesIcon from '@bitrix24/b24icons-vue/actions/CopyPlatesIcon'
import PencilDrawIcon from '@bitrix24/b24icons-vue/actions/PencilDrawIcon'

const items: ContextMenuItem[][] = [
  [
    {
      label: 'View',
      icon: OpenedEyeIcon
    },
    {
      label: 'Copy',
      icon: CopyPlatesIcon
    },
    {
      label: 'Edit',
      icon: PencilDrawIcon
    }
  ],
  [
    {
      label: 'Delete',
      color: 'air-primary-alert' as const,
      icon: TrashBinIcon
    }
  ]
]
</script>

<template>
  <B24ContextMenu :items="items">
    <div class="flex items-center justify-center rounded-md border border-dashed border-(--ui-color-design-outline-na-stroke) text-(--b24ui-typography-legend-color) text-(length:--ui-font-size-sm) bg-(--ui-color-bg-content-primary) aspect-video w-72">
      Right click here
    </div>
  </B24ContextMenu>
</template>

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 '@bitrix24/b24ui-nuxt'
import SpinnerIcon from '@bitrix24/b24icons-vue/specialized/SpinnerIcon'

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 ContextMenuItem[]
</script>

<template>
  <B24ContextMenu :items="items">
    <div class="flex items-center justify-center rounded-md border border-dashed border-(--ui-color-design-outline-na-stroke) text-(--b24ui-typography-legend-color) text-(length:--ui-font-size-sm) bg-(--ui-color-bg-content-primary) aspect-video w-72">
      Right click here
    </div>

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

    <template #refresh-trailing>
      <SpinnerIcon v-if="loading" class="shrink-0 size-5 text-(--ui-color-accent-main-primary) animate-spin" />
    </template>
  </B24ContextMenu>
</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, S, D, U, I, C and J would trigger the select function of the corresponding item.

API

Props

Prop Default Type
items T
checkedIconicons.checkIconComponent

The icon displayed when an item is checked.

loadingIconicons.loadingIconComponent

The icon displayed when an item is loading.

externalIcontrueIconComponent

The icon displayed when the item is an external link. Set to false to hide the external icon.

content ContextMenuContentProps & Partial<EmitsToProps<MenuContentEmits>>

The content of the menu.

portaltrue string | false | true | HTMLElement

Render the menu in a portal.

labelKey'label' keyof Extract<NestedItem<T>, object> & string | DotPathKeys<Extract<NestedItem<T>, object>>

The key used to get the label from the item.

descriptionKey'description' keyof Extract<NestedItem<T>, object> & string | DotPathKeys<Extract<NestedItem<T>, object>>

The key used to get the description from the item.

disabledboolean
modaltrueboolean

The modality of the dropdown menu.

When set to true, interaction with outside elements will be disabled and only menu content will be visible to screen readers.

pressOpenDelay700 number

The duration from when the trigger is pressed until the menu opens.

b24ui { content?: ClassNameValue; viewport?: ClassNameValue; group?: ClassNameValue; label?: ClassNameValue; separator?: ClassNameValue; item?: ClassNameValue; itemLeadingIcon?: ClassNameValue; itemLeadingAvatar?: ClassNameValue; itemLeadingAvatarSize?: ClassNameValue; itemTrailing?: ClassNameValue; itemTrailingIcon?: ClassNameValue; itemTrailingKbds?: ClassNameValue; itemWrapper?: ClassNameValue; itemTrailingKbdsSize?: ClassNameValue; itemLabel?: ClassNameValue; itemDescription?: ClassNameValue; itemLabelExternalIcon?: ClassNameValue; }

Slots

Slot Type
default{}
item{ item: NestedItem<T>; active: boolean; index: number; b24ui: object; }
item-leading{ item: NestedItem<T>; active: boolean; index: number; b24ui: object; }
item-label{ item: NestedItem<T>; active: boolean; index: number; }
item-description{ item: NestedItem<T>; active: boolean; index: number; }
item-trailing{ item: NestedItem<T>; active: boolean; index: number; b24ui: object; }
content-top{ sub: boolean; }
content-bottom{ sub: boolean; }

Emits

Event Type
update:open[payload: boolean]

Theme

app.config.ts
export default defineAppConfig({
  b24ui: {
    contextMenu: {
      slots: {
        content: 'base-mode bg-(--ui-color-bg-content-primary) shadow-(--popup-window-box-shadow) rounded-(--ui-border-radius-xl) will-change-[opacity] motion-safe:data-[state=open]:animate-[scale-in_100ms_ease-out] motion-safe:data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-context-menu-content-transform-origin) font-[family-name:var(--ui-font-family-primary)] relative isolate px-0 py-(--menu-popup-padding) pointer-events-auto',
        viewport: 'relative w-full max-h-[40vh] min-w-[192px] scroll-py-1 overflow-x-hidden overflow-y-auto scrollbar-thin',
        group: 'grid',
        label: 'w-full h-(--popup-window-delimiter-section-height) px-[18px] mt-(--menu-item-block-stack-space) flex flex-row rtl:flex-row-reverse items-center select-none outline-none whitespace-nowrap text-start text-(length:--ui-size-sm) text-(--b24ui-typography-legend-color) font-(--ui-font-weight-normal) after:ms-[10px] after:block after:flex-1 after:min-w-[15px] after:h-px after:bg-(--ui-color-divider-vibrant-default)',
        separator: 'my-[8px] mx-[18px] h-[1px] bg-(--ui-color-divider-vibrant-default)',
        item: 'group w-full h-[36px] px-[18px] mt-(--menu-item-block-stack-space) relative flex flex-row rtl:flex-row-reverse items-center select-none outline-none whitespace-nowrap cursor-pointer data-disabled:cursor-not-allowed data-disabled:opacity-30 text-start text-(length:--ui-font-size-md) text-(--b24ui-typography-legend-color) hover:text-(--b24ui-typography-label-color) data-highlighted:text-(--b24ui-typography-label-color) data-[state=open]:text-(--b24ui-typography-label-color) hover:bg-(--ui-color-divider-optical-1-alt) data-highlighted:bg-(--ui-color-divider-optical-1-alt) data-[state=open]:bg-(--ui-color-divider-optical-1-alt) transition-colors',
        itemLeadingIcon: 'shrink-0 size-[18px] text-(--ui-color-design-plain-content-icon-secondary) group-data-highlighted:text-(--ui-color-accent-main-primary) group-data-[state=open]:text-(--ui-color-accent-main-primary) group-data-[state=checked]:text-(--ui-color-accent-main-primary) transition-colors',
        itemLeadingAvatar: 'shrink-0 size-[16px] me-[12px]',
        itemLeadingAvatarSize: '2xs',
        itemTrailing: 'ml-auto rtl:ml-0 rtl:mr-auto inline-flex gap-1.5 items-center',
        itemTrailingIcon: 'shrink-0 size-[24px] text-(--ui-color-accent-main-primary)',
        itemTrailingKbds: 'shrink-0 hidden lg:inline-flex items-center gap-0.5',
        itemWrapper: 'flex-1 flex flex-col text-start min-w-0',
        itemTrailingKbdsSize: 'md',
        itemLabel: 'max-w-[240px] truncate -mt-px group-data-[state=checked]:text-(--ui-color-accent-main-primary)',
        itemDescription: 'max-w-[240px] truncate -mt-[6px] text-(--b24ui-typography-description-color) text-(length:--ui-font-size-sm)',
        itemLabelExternalIcon: 'inline-block size-[16px] text-(--ui-color-design-plain-content-icon-secondary)'
      },
      variants: {
        color: {
          'air-primary': {
            item: 'style-filled'
          },
          'air-primary-success': {
            item: 'style-filled-success'
          },
          'air-primary-alert': {
            item: 'style-filled-alert'
          },
          'air-primary-copilot': {
            item: 'style-filled-copilot'
          },
          'air-primary-warning': {
            item: 'style-filled-warning'
          }
        },
        active: {
          true: {
            item: 'text-(--ui-color-accent-main-primary) hover:text-(--ui-color-accent-main-primary)',
            itemLeadingIcon: 'text-(--ui-color-accent-main-primary) hover:text-(--ui-color-accent-main-primary) group-data-[state=open]:text-(--ui-color-accent-main-primary)'
          },
          false: {}
        },
        loading: {
          true: {
            itemLeadingIcon: 'animate-spin'
          }
        }
      },
      compoundVariants: [],
      defaultVariants: {}
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import bitrix24UIPluginVite from '@bitrix24/b24ui-nuxt/vite'

export default defineConfig({
  plugins: [
    vue(),
    bitrix24UIPluginVite({
      b24ui: {
        contextMenu: {
          slots: {
            content: 'base-mode bg-(--ui-color-bg-content-primary) shadow-(--popup-window-box-shadow) rounded-(--ui-border-radius-xl) will-change-[opacity] motion-safe:data-[state=open]:animate-[scale-in_100ms_ease-out] motion-safe:data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-context-menu-content-transform-origin) font-[family-name:var(--ui-font-family-primary)] relative isolate px-0 py-(--menu-popup-padding) pointer-events-auto',
            viewport: 'relative w-full max-h-[40vh] min-w-[192px] scroll-py-1 overflow-x-hidden overflow-y-auto scrollbar-thin',
            group: 'grid',
            label: 'w-full h-(--popup-window-delimiter-section-height) px-[18px] mt-(--menu-item-block-stack-space) flex flex-row rtl:flex-row-reverse items-center select-none outline-none whitespace-nowrap text-start text-(length:--ui-size-sm) text-(--b24ui-typography-legend-color) font-(--ui-font-weight-normal) after:ms-[10px] after:block after:flex-1 after:min-w-[15px] after:h-px after:bg-(--ui-color-divider-vibrant-default)',
            separator: 'my-[8px] mx-[18px] h-[1px] bg-(--ui-color-divider-vibrant-default)',
            item: 'group w-full h-[36px] px-[18px] mt-(--menu-item-block-stack-space) relative flex flex-row rtl:flex-row-reverse items-center select-none outline-none whitespace-nowrap cursor-pointer data-disabled:cursor-not-allowed data-disabled:opacity-30 text-start text-(length:--ui-font-size-md) text-(--b24ui-typography-legend-color) hover:text-(--b24ui-typography-label-color) data-highlighted:text-(--b24ui-typography-label-color) data-[state=open]:text-(--b24ui-typography-label-color) hover:bg-(--ui-color-divider-optical-1-alt) data-highlighted:bg-(--ui-color-divider-optical-1-alt) data-[state=open]:bg-(--ui-color-divider-optical-1-alt) transition-colors',
            itemLeadingIcon: 'shrink-0 size-[18px] text-(--ui-color-design-plain-content-icon-secondary) group-data-highlighted:text-(--ui-color-accent-main-primary) group-data-[state=open]:text-(--ui-color-accent-main-primary) group-data-[state=checked]:text-(--ui-color-accent-main-primary) transition-colors',
            itemLeadingAvatar: 'shrink-0 size-[16px] me-[12px]',
            itemLeadingAvatarSize: '2xs',
            itemTrailing: 'ml-auto rtl:ml-0 rtl:mr-auto inline-flex gap-1.5 items-center',
            itemTrailingIcon: 'shrink-0 size-[24px] text-(--ui-color-accent-main-primary)',
            itemTrailingKbds: 'shrink-0 hidden lg:inline-flex items-center gap-0.5',
            itemWrapper: 'flex-1 flex flex-col text-start min-w-0',
            itemTrailingKbdsSize: 'md',
            itemLabel: 'max-w-[240px] truncate -mt-px group-data-[state=checked]:text-(--ui-color-accent-main-primary)',
            itemDescription: 'max-w-[240px] truncate -mt-[6px] text-(--b24ui-typography-description-color) text-(length:--ui-font-size-sm)',
            itemLabelExternalIcon: 'inline-block size-[16px] text-(--ui-color-design-plain-content-icon-secondary)'
          },
          variants: {
            color: {
              'air-primary': {
                item: 'style-filled'
              },
              'air-primary-success': {
                item: 'style-filled-success'
              },
              'air-primary-alert': {
                item: 'style-filled-alert'
              },
              'air-primary-copilot': {
                item: 'style-filled-copilot'
              },
              'air-primary-warning': {
                item: 'style-filled-warning'
              }
            },
            active: {
              true: {
                item: 'text-(--ui-color-accent-main-primary) hover:text-(--ui-color-accent-main-primary)',
                itemLeadingIcon: 'text-(--ui-color-accent-main-primary) hover:text-(--ui-color-accent-main-primary) group-data-[state=open]:text-(--ui-color-accent-main-primary)'
              },
              false: {}
            },
            loading: {
              true: {
                itemLeadingIcon: 'animate-spin'
              }
            }
          },
          compoundVariants: [],
          defaultVariants: {}
        }
      }
    })
  ]
})
Some colors in compoundVariants are omitted for readability. Check out the source code on GitHub.
Releases
Published under MIT License.

Copyright © 2024-present Bitrix24