Skip to content

Stopping Vuetify's menu cascade with useMenuBoundary

vue3

The problem

A <v-select> inside a v-dialog that was opened from a v-menu. Click the select, and the whole dialog closes — unsaved changes and all.

Vuetify's v-menu uses provide/inject so nested menus can find their nearest ancestor and cascade-close on an outside click:

const parent = inject(VMenuSymbol, null)

provide(VMenuSymbol, {
  register()    { /* add self to parent's children */ },
  unregister()  { /* remove self */ },
  closeParents(e) {
    if (clickedOutside && !openChildren.value.size && !props.persistent) {
      isActive.value = false   // close yourself
      parent?.closeParents(e)  // cascade upward
    }
  }
})

v-dialog doesn't provide VMenuSymbol, so the select injects the outer menu as its parent. An outside click on the select closes that outer menu — unmounting the dialog with it.

The solution

Provide a no-op stand-in at the dialog boundary:

import { provide } from 'vue'

const VMenuSymbol = Symbol.for('vuetify:v-menu')

export function useMenuBoundary() {
    provide(VMenuSymbol, {
        register() {},
        unregister() {},
        closeParents() {},
    })
}

Why it works

  • The select's menu now injects this stub and hits a no-op, so the cascade stops at the dialog.
  • It doesn't forward to a real parent, so the outer menu's own click-outside still works.
  • Symbol.for (not Symbol()) matches Vuetify's globally-registered key, so the inject actually resolves to the stub.
  • No forks, no hacks — it plugs into Vuetify's own DI contract, and it's a one-line opt-in.

provide/inject is a graph of invisible contracts — sometimes the fix is just a wall where the framework assumed there was a menu.

End of article